Skip to content

Commit 9cb291b

Browse files
committed
Merge remote-tracking branch 'origin/release/9.7.0' into trunk
2 parents 96a3da8 + 892f96d commit 9cb291b

File tree

74 files changed

+4854
-1725
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

74 files changed

+4854
-1725
lines changed

changelog.txt

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

3+
= 9.7.0 - 2025-07-21 =
4+
* Update - Removes BNPL payment methods (Klarna and Affirm) when other official plugins are active
5+
* Fix - Moves the existing order lock functionality earlier in the order processing flow to prevent duplicate processing requests
6+
* Add - Adds two new safety filters to the subscriptions detached debug tool: `wc_stripe_detached_subscriptions_maximum_time` and `wc_stripe_detached_subscriptions_maximum_count`
7+
* Add - Show a notice when editing an active subscription that has no payment method attached
8+
* Fix - Fixes a possible fatal error when trying to generate the order signature for a `WC_Order_Refund` object
9+
* Add - New WooCommerce Debug Tool to list subscriptions without a payment method attached
10+
* Fix - Fixes a possible error notice when the `payment_request` Stripe setting key is not defined
11+
* Fix - Prevent irrelevant payment method update requests to Stripe during checkout
12+
* Add - A notice to take user back to WC onboarding flow after connecting the Stripe account
13+
* Tweak - Deprecate wc_connect_* filters
14+
* Fix - Prevent text field reset while editing Optimized Checkout title
15+
* Update - Improvements to custom checkout fields support for express checkout
16+
* Tweak - Use the Database Cache for the Stripe Account Data
17+
* Update - Update filter names to use the wc_stripe_* prefix
18+
* Add - Show payment methods sync status on the UI
19+
* Fix - No such customer error when creating a payment method with a new Stripe account
20+
* Fix - Validate create customer payload against required billing fields before sending to Stripe
21+
* Update - Enhanced logging system with support for all log levels and improved context handling
22+
* Fix - Fixes wrong price formatting in express checkout
23+
* Fix - Require email address only for Stripe customer validation when request is from the Add Payment Method page
24+
* Fix - Set default values for custom field options
25+
* Fix - Enforce rate limiter for failed add payment method attempts
26+
* Update - Add the number of pending webhooks to the Account status section
27+
* Fix - Prevent "Undefined array key charges_enabled" PHP warning when determining live‑mode status
28+
* Update - Deprecate `wc_gateway_stripe_process_payment`, `wc_gateway_stripe_process_redirect_payment` and `wc_gateway_stripe_process_webhook_payment` actions in favour of `wc_gateway_stripe_process_payment_charge`
29+
330
= 9.6.0 - 2025-07-07 =
31+
* Fix - Register Express Checkout script before use to restore buttons on “order-pay” pages
432
* Dev - Re-include the deprecated WC_Stripe_Order class to avoid breaking changes for merchants using it
533
* Update - Removes the change display order feature from the settings page when the Optimized Checkout is enabled
634
* Update - Removes the customization of individual payment method titles and descriptions

client/blocks/upe/index.js

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,11 @@ import {
33
registerExpressPaymentMethod,
44
} from '@woocommerce/blocks-registry';
55
import {
6+
PAYMENT_METHOD_AFFIRM,
67
PAYMENT_METHOD_AMAZON_PAY,
78
PAYMENT_METHOD_CARD,
89
PAYMENT_METHOD_GIROPAY,
10+
PAYMENT_METHOD_KLARNA,
911
PAYMENT_METHOD_LINK,
1012
} from '../../stripe-utils/constants';
1113
import { updateTokenLabelsWhenLoaded } from './token-label-updater.js';
@@ -38,18 +40,26 @@ const api = new WCStripeAPI(
3840
const paymentMethodsConfig =
3941
getBlocksConfiguration()?.paymentMethodsConfig ?? {};
4042

41-
const methodsToFilter = [
42-
PAYMENT_METHOD_AMAZON_PAY,
43-
PAYMENT_METHOD_LINK,
44-
PAYMENT_METHOD_GIROPAY, // Skip giropay as it was deprecated by Jun, 30th 2024.
45-
];
46-
4743
// Register UPE Elements.
4844
if ( getBlocksConfiguration()?.isOCEnabled ) {
4945
registerPaymentMethod(
5046
upeElement( PAYMENT_METHOD_CARD, api, paymentMethodsConfig.card )
5147
);
5248
} else {
49+
const methodsToFilter = [
50+
PAYMENT_METHOD_AMAZON_PAY,
51+
PAYMENT_METHOD_LINK,
52+
PAYMENT_METHOD_GIROPAY, // Skip giropay as it was deprecated by Jun, 30th 2024.
53+
];
54+
55+
// Filter out some BNPLs when other official extensions are present.
56+
if ( getBlocksConfiguration()?.hasAffirmGatewayPlugin ) {
57+
methodsToFilter.push( PAYMENT_METHOD_AFFIRM );
58+
}
59+
if ( getBlocksConfiguration()?.hasKlarnaGatewayPlugin ) {
60+
methodsToFilter.push( PAYMENT_METHOD_KLARNA );
61+
}
62+
5363
Object.entries( paymentMethodsConfig )
5464
.filter( ( [ method ] ) => ! methodsToFilter.includes( method ) )
5565
.forEach( ( [ method, config ] ) => {
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import React from 'react';
2+
import { screen, render } from '@testing-library/react';
3+
import PaymentMethodUnavailableDueConflictPill from '..';
4+
import { PAYMENT_METHOD_AFFIRM } from 'wcstripe/stripe-utils/constants';
5+
6+
describe( 'PaymentMethodUnavailableDueConflictPill', () => {
7+
beforeEach( () => {
8+
global.wc_stripe_settings_params = { has_affirm_gateway_plugin: false };
9+
} );
10+
11+
it( 'should render the "Has plugin conflict" text', () => {
12+
global.wc_stripe_settings_params = { has_affirm_gateway_plugin: true };
13+
14+
render(
15+
<PaymentMethodUnavailableDueConflictPill
16+
id={ PAYMENT_METHOD_AFFIRM }
17+
label="Affirm"
18+
/>
19+
);
20+
21+
expect(
22+
screen.queryByText( 'Has plugin conflict' )
23+
).toBeInTheDocument();
24+
} );
25+
26+
it( 'should not render when other extensions are not active', () => {
27+
const { container } = render(
28+
<PaymentMethodUnavailableDueConflictPill
29+
id={ PAYMENT_METHOD_AFFIRM }
30+
label="Affirm"
31+
/>
32+
);
33+
34+
expect( container.firstChild ).toBeNull();
35+
} );
36+
} );
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
/* global wc_stripe_settings_params */
2+
import { __, sprintf } from '@wordpress/i18n';
3+
import React from 'react';
4+
import styled from '@emotion/styled';
5+
import interpolateComponents from 'interpolate-components';
6+
import { Icon, info } from '@wordpress/icons';
7+
import Popover from 'wcstripe/components/popover';
8+
import {
9+
PAYMENT_METHOD_AFFIRM,
10+
PAYMENT_METHOD_KLARNA,
11+
} from 'wcstripe/stripe-utils/constants';
12+
13+
const StyledPill = styled.span`
14+
display: inline-flex;
15+
align-items: center;
16+
gap: 4px;
17+
padding: 4px 8px;
18+
border: 1px solid #fcf9e8;
19+
border-radius: 2px;
20+
background-color: #fcf9e8;
21+
color: #674600;
22+
font-size: 12px;
23+
font-weight: 400;
24+
line-height: 16px;
25+
width: fit-content;
26+
`;
27+
28+
const StyledLink = styled.a`
29+
&:focus,
30+
&:visited {
31+
box-shadow: none;
32+
}
33+
`;
34+
35+
const IconWrapper = styled.span`
36+
height: 16px;
37+
cursor: pointer;
38+
`;
39+
40+
const AlertIcon = styled( Icon )`
41+
fill: #674600;
42+
`;
43+
44+
const IconComponent = ( { children, ...props } ) => (
45+
<IconWrapper { ...props }>
46+
<AlertIcon icon={ info } size="16" />
47+
{ children }
48+
</IconWrapper>
49+
);
50+
51+
const PaymentMethodUnavailableDueConflictPill = ( { id, label } ) => {
52+
if (
53+
( id === PAYMENT_METHOD_AFFIRM &&
54+
// eslint-disable-next-line camelcase
55+
wc_stripe_settings_params.has_affirm_gateway_plugin ) ||
56+
( id === PAYMENT_METHOD_KLARNA &&
57+
// eslint-disable-next-line camelcase
58+
wc_stripe_settings_params.has_klarna_gateway_plugin )
59+
) {
60+
return (
61+
<StyledPill>
62+
{ __( 'Has plugin conflict', 'woocommerce-gateway-stripe' ) }
63+
<Popover
64+
BaseComponent={ IconComponent }
65+
content={ interpolateComponents( {
66+
mixedString: sprintf(
67+
/* translators: $1: a payment method name */
68+
__(
69+
'%1$s is unavailable due to another official plugin being active.',
70+
'woocommerce-gateway-stripe'
71+
),
72+
label
73+
),
74+
components: {
75+
currencySettingsLink: (
76+
<StyledLink
77+
href="/wp-admin/admin.php?page=wc-settings&tab=general"
78+
target="_blank"
79+
rel="noreferrer"
80+
onClick={ ( ev ) => {
81+
// Stop propagation is necessary so it doesn't trigger the tooltip click event.
82+
ev.stopPropagation();
83+
} }
84+
/>
85+
),
86+
},
87+
} ) }
88+
/>
89+
</StyledPill>
90+
);
91+
}
92+
93+
return null;
94+
};
95+
96+
export default PaymentMethodUnavailableDueConflictPill;

client/data/settings/hooks.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,10 @@ export const useDebugLog = makeSettingsHook( 'is_debug_log_enabled' );
167167
export const useIsUpeEnabled = makeSettingsHook( 'is_upe_enabled' );
168168
export const useIsOCEnabled = makeSettingsHook( 'is_oc_enabled' );
169169
export const useOCTitle = makeSettingsHook( 'oc_title', 'Stripe' );
170+
export const useIsPMCEnabled = makeReadOnlySettingsHook(
171+
'is_pmc_enabled',
172+
false
173+
);
170174

171175
export const useIndividualPaymentMethodSettings = makeSettingsHook(
172176
'individual_payment_method_settings',

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,12 @@ import {
1414

1515
jest.mock( '@woocommerce/blocks-checkout', () => {}, { virtual: true } );
1616

17+
jest.mock( 'wcstripe/stripe-utils', () => ( {
18+
getStripeServerData: jest.fn( () => ( {
19+
isCheckout: true,
20+
} ) ),
21+
} ) );
22+
1723
describe( 'Express checkout event handlers', () => {
1824
describe( 'shippingAddressChangeHandler', () => {
1925
let api;

client/express-checkout/compatibility/classic-checkout-custom-fields.js

Lines changed: 16 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -23,28 +23,26 @@ addFilter(
2323

2424
// Extract the data from the checkout form.
2525
const customCheckoutFieldsData = {};
26-
Object.keys( customCheckoutFields ).forEach( ( field ) => {
27-
const formElements = document.querySelectorAll(
28-
`form[name="checkout"] [name="${ field }"]`
29-
);
30-
if ( ! formElements || formElements.length === 0 ) {
31-
return;
32-
}
26+
const form = document.querySelector( 'form[name="checkout"]' );
27+
if ( ! form ) {
28+
return extensionData;
29+
}
3330

34-
formElements.forEach( ( formElement ) => {
35-
if ( formElement.type === 'checkbox' ) {
36-
if ( formElement.checked ) {
37-
customCheckoutFieldsData[ field ] = 1;
38-
}
39-
} else if ( formElement.type === 'radio' ) {
40-
if ( formElement.checked ) {
41-
customCheckoutFieldsData[ field ] = formElement.value;
31+
const formData = new FormData( form );
32+
for ( const [ fieldName, fieldValue ] of formData.entries() ) {
33+
const isMultiSelect = fieldName.endsWith( '[]' );
34+
const key = isMultiSelect ? fieldName.slice( 0, -2 ) : fieldName;
35+
if ( Object.keys( customCheckoutFields ).includes( key ) ) {
36+
if ( isMultiSelect ) {
37+
if ( ! customCheckoutFieldsData[ key ] ) {
38+
customCheckoutFieldsData[ key ] = [];
4239
}
40+
customCheckoutFieldsData[ key ].push( fieldValue );
4341
} else {
44-
customCheckoutFieldsData[ field ] = formElement.value;
42+
customCheckoutFieldsData[ key ] = fieldValue;
4543
}
46-
} );
47-
} );
44+
}
45+
}
4846

4947
return {
5048
...extensionData,

client/express-checkout/utils/__tests__/normalize.test.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,12 @@ jest.mock( 'wcstripe/express-checkout/utils', () => ( {
1212
getExpressCheckoutData: jest.fn(),
1313
} ) );
1414

15+
jest.mock( 'wcstripe/stripe-utils', () => ( {
16+
getStripeServerData: jest.fn( () => ( {
17+
isCheckout: true,
18+
} ) ),
19+
} ) );
20+
1521
describe( 'Express checkout normalization', () => {
1622
describe( 'normalizeLineItems', () => {
1723
test( 'normalizes blocks array properly', () => {

client/express-checkout/utils/normalize.js

Lines changed: 29 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { select } from '@wordpress/data';
22
import { applyFilters } from '@wordpress/hooks';
33
import { getExpressCheckoutData } from 'wcstripe/express-checkout/utils';
4+
import { getStripeServerData } from 'wcstripe/stripe-utils';
45

56
/**
67
* Normalizes incoming cart total items for use as a displayItems with the Stripe api.
@@ -152,12 +153,18 @@ const approximateLastName = ( name, defaultValue = '' ) => {
152153
* @return {Object} The custom billing address data.
153154
*/
154155
const getCustomBillingAddressData = ( data ) => {
156+
// Bail if we're on classic checkout. We do not use the Checkout/Cart Store
157+
// for supporting custom fields in classic checkout.
158+
if ( getStripeServerData()?.isCheckout && ! isBlockCheckoutPage() ) {
159+
return {};
160+
}
161+
155162
// We need to specifically pass empty fields when not on the block checkout page,
156163
// to avoid sending "hidden" and possibly stale data from previous transactions,
157164
// e.g. shopper is on the product page (hence, no checkout form fields are displayed),
158165
// but session still holds data from a previous checkout.
159166
if ( ! isBlockCheckoutPage() ) {
160-
return emptyCustomFieldObject( [ 'address' ] );
167+
return emptyCustomFieldsData( [ 'address' ] );
161168
}
162169

163170
const customerData = getCustomerDataFromStore();
@@ -189,12 +196,18 @@ const getCustomBillingAddressData = ( data ) => {
189196
* @return {Object} The custom shipping address data.
190197
*/
191198
const getCustomShippingAddressData = ( data ) => {
199+
// Bail if we're on classic checkout. We do not use the Checkout/Cart Store
200+
// for supporting custom fields in classic checkout.
201+
if ( getStripeServerData()?.isCheckout && ! isBlockCheckoutPage() ) {
202+
return {};
203+
}
204+
192205
// We need to specifically pass empty fields when not on the block checkout page,
193206
// to avoid sending "hidden" and possibly stale data from previous transactions,
194207
// e.g. shopper is on the product page (hence, no checkout form fields are displayed),
195208
// but session still holds data from a previous checkout.
196209
if ( ! isBlockCheckoutPage() ) {
197-
return emptyCustomFieldObject( [ 'address' ] );
210+
return emptyCustomFieldsData( [ 'address' ] );
198211
}
199212

200213
const customerData = getCustomerDataFromStore();
@@ -258,12 +271,18 @@ const getPhone = ( event ) => {
258271
* @return {Object} The additional fields data.
259272
*/
260273
const getAdditionalFieldsData = () => {
274+
// Bail if we're on classic checkout. We do not use the Checkout/Cart Store
275+
// for supporting custom fields in classic checkout.
276+
if ( getStripeServerData()?.isCheckout && ! isBlockCheckoutPage() ) {
277+
return {};
278+
}
279+
261280
// We need to specifically pass empty fields when not on the block checkout page,
262281
// to avoid sending "hidden" and possibly stale data from previous transactions,
263282
// e.g. shopper is on the product page (hence, no checkout form fields are displayed),
264283
// but session still holds data from a previous checkout.
265284
if ( ! isBlockCheckoutPage() ) {
266-
return emptyCustomFieldObject( [ 'contact', 'order' ] );
285+
return emptyCustomFieldsData( [ 'contact', 'order' ] );
267286
}
268287

269288
return getAdditionalFieldsDataFromStore();
@@ -315,13 +334,15 @@ const getExtensionDataFromStore = () => {
315334
*
316335
* @return {Object} The custom fields object with empty values.
317336
*/
318-
const emptyCustomFieldObject = ( locations ) => {
319-
const customFields = getExpressCheckoutData( 'custom_checkout_fields' );
320-
if ( ! customFields ) {
337+
const emptyCustomFieldsData = ( locations ) => {
338+
const customCheckoutFields = getExpressCheckoutData(
339+
'custom_checkout_fields'
340+
);
341+
if ( ! customCheckoutFields ) {
321342
return {};
322343
}
323344

324-
const customFieldObject = Object.entries( customFields ).reduce(
345+
const emptyData = Object.entries( customCheckoutFields ).reduce(
325346
( acc, [ field, config ] ) => {
326347
if ( locations.includes( config.location ) ) {
327348
acc[ field ] = '';
@@ -331,7 +352,7 @@ const emptyCustomFieldObject = ( locations ) => {
331352
{}
332353
);
333354

334-
return customFieldObject;
355+
return emptyData;
335356
};
336357

337358
/**

0 commit comments

Comments
 (0)