Skip to content

Commit 0f24715

Browse files
authored
ECE: Fix checkout fails for countries without states (#4451)
* add helper methods * modify country locale for countries with empty states * add nonce verification * add changelog * update changelog * add tests * ignore phpcs
1 parent e8087e1 commit 0f24715

8 files changed

+345
-18
lines changed

changelog.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@
5050
* Fix - Fix payment processing for $0 subscription with recurring coupon
5151
* Dev - Add e2e tests to cover Affirm purchase flow
5252
* Fix - Add safety check when checking error object
53+
* Fix - Correctly handle countries without states when using the express payment methods
5354

5455
= 9.5.3 - 2025-06-23 =
5556
* Fix - Reimplement mapping of Express Checkout state values to align with WooCommerce's expected state formats

client/api/index.js

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -625,10 +625,19 @@ export default class WCStripeAPI {
625625
* @return {Promise} Promise for the request to the server.
626626
*/
627627
expressCheckoutECECreateOrder( orderData ) {
628-
return this.postToBlocksAPI( '/wc/store/v1/checkout', {
629-
...orderData,
630-
customer_note: getCustomerNote(),
631-
} );
628+
return this.postToBlocksAPI(
629+
'/wc/store/v1/checkout',
630+
{
631+
...orderData,
632+
customer_note: getCustomerNote(),
633+
},
634+
{
635+
'X-WCSTRIPE-EXPRESS-CHECKOUT': true,
636+
'X-WCSTRIPE-EXPRESS-CHECKOUT-NONCE': getExpressCheckoutData(
637+
'nonce'
638+
)?.wc_store_api_express_checkout,
639+
}
640+
);
632641
}
633642

634643
/**
@@ -653,14 +662,16 @@ export default class WCStripeAPI {
653662
*
654663
* @param {string} path The path to post to.
655664
* @param {Object} data The data to post.
665+
* @param {Object} headers The headers for the request.
656666
* @return {Promise} The promise for the request to the server.
657667
*/
658-
postToBlocksAPI( path, data ) {
668+
postToBlocksAPI( path, data, headers = {} ) {
659669
return apiFetch( {
660670
method: 'POST',
661671
path,
662672
headers: {
663673
Nonce: getExpressCheckoutData( 'nonce' )?.wc_store_api,
674+
...headers,
664675
},
665676
data,
666677
} );

includes/payment-methods/class-wc-stripe-express-checkout-ajax-handler.php

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ public function init() {
3838
add_action( 'wc_ajax_wc_stripe_clear_cart', [ $this, 'ajax_clear_cart' ] );
3939
add_action( 'wc_ajax_wc_stripe_log_errors', [ $this, 'ajax_log_errors' ] );
4040
add_action( 'wc_ajax_wc_stripe_pay_for_order', [ $this, 'ajax_pay_for_order' ] );
41+
add_filter( 'woocommerce_get_country_locale', [ $this, 'modify_country_locale_for_express_checkout' ], 20 );
4142
}
4243

4344
/**
@@ -388,4 +389,29 @@ public function ajax_pay_for_order() {
388389

389390
wp_send_json( $result );
390391
}
392+
393+
/**
394+
* Modify country locale for express checkout.
395+
* Countries that don't have state fields, make the state field optional.
396+
*
397+
* @param array $locale The country locale.
398+
* @return array Modified country locale.
399+
*/
400+
public function modify_country_locale_for_express_checkout( $locale ) {
401+
// Only modify locale settings if this is an express checkout context.
402+
if ( ! $this->express_checkout_helper->is_express_checkout_context() ) {
403+
return $locale;
404+
}
405+
406+
include_once WC_STRIPE_PLUGIN_PATH . '/includes/constants/class-wc-stripe-payment-request-button-states.php';
407+
408+
// For countries that don't have state fields, make the state field optional.
409+
foreach ( WC_Stripe_Payment_Request_Button_States::STATES as $country_code => $states ) {
410+
if ( empty( $states ) ) {
411+
$locale[ $country_code ]['state']['required'] = false;
412+
}
413+
}
414+
415+
return $locale;
416+
}
391417
}

includes/payment-methods/class-wc-stripe-express-checkout-element.php

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -341,18 +341,19 @@ public function javascript_params() {
341341
'is_payment_request_enabled' => $this->express_checkout_helper->is_payment_request_enabled(),
342342
],
343343
'nonce' => [
344-
'payment' => wp_create_nonce( 'wc-stripe-express-checkout' ),
345-
'shipping' => wp_create_nonce( 'wc-stripe-express-checkout-shipping' ),
346-
'normalize_address' => wp_create_nonce( 'wc-stripe-express-checkout-normalize-address' ),
347-
'get_cart_details' => wp_create_nonce( 'wc-stripe-get-cart-details' ),
348-
'update_shipping' => wp_create_nonce( 'wc-stripe-update-shipping-method' ),
349-
'checkout' => wp_create_nonce( 'woocommerce-process_checkout' ),
350-
'add_to_cart' => wp_create_nonce( 'wc-stripe-add-to-cart' ),
351-
'get_selected_product_data' => wp_create_nonce( 'wc-stripe-get-selected-product-data' ),
352-
'log_errors' => wp_create_nonce( 'wc-stripe-log-errors' ),
353-
'clear_cart' => wp_create_nonce( 'wc-stripe-clear-cart' ),
354-
'pay_for_order' => wp_create_nonce( 'wc-stripe-pay-for-order' ),
355-
'wc_store_api' => wp_create_nonce( 'wc_store_api' ),
344+
'payment' => wp_create_nonce( 'wc-stripe-express-checkout' ),
345+
'shipping' => wp_create_nonce( 'wc-stripe-express-checkout-shipping' ),
346+
'normalize_address' => wp_create_nonce( 'wc-stripe-express-checkout-normalize-address' ),
347+
'get_cart_details' => wp_create_nonce( 'wc-stripe-get-cart-details' ),
348+
'update_shipping' => wp_create_nonce( 'wc-stripe-update-shipping-method' ),
349+
'checkout' => wp_create_nonce( 'woocommerce-process_checkout' ),
350+
'add_to_cart' => wp_create_nonce( 'wc-stripe-add-to-cart' ),
351+
'get_selected_product_data' => wp_create_nonce( 'wc-stripe-get-selected-product-data' ),
352+
'log_errors' => wp_create_nonce( 'wc-stripe-log-errors' ),
353+
'clear_cart' => wp_create_nonce( 'wc-stripe-clear-cart' ),
354+
'pay_for_order' => wp_create_nonce( 'wc-stripe-pay-for-order' ),
355+
'wc_store_api' => wp_create_nonce( 'wc_store_api' ),
356+
'wc_store_api_express_checkout' => wp_create_nonce( 'wc_store_api_express_checkout' ),
356357
],
357358
'i18n' => [
358359
'no_prepaid_card' => __( 'Sorry, we\'re not accepting prepaid cards at this time.', 'woocommerce-gateway-stripe' ),

includes/payment-methods/class-wc-stripe-express-checkout-helper.php

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1641,4 +1641,41 @@ public function get_booking_id_from_cart() {
16411641

16421642
return false;
16431643
}
1644+
1645+
/**
1646+
* Check if the current request is an express checkout context.
1647+
*
1648+
* @return bool True if express checkout context, false otherwise.
1649+
*/
1650+
public function is_express_checkout_context() {
1651+
// Only proceed if this is a Store API request.
1652+
if ( ! $this->is_request_to_store_api() ) {
1653+
return false;
1654+
}
1655+
1656+
// Check for the 'X-WCSTRIPE-EXPRESS-CHECKOUT' header using superglobals.
1657+
if ( 'true' !== sanitize_text_field( wp_unslash( $_SERVER['HTTP_X_WCSTRIPE_EXPRESS_CHECKOUT'] ?? '' ) ) ) {
1658+
return false;
1659+
}
1660+
1661+
// Check for the 'X-WCSTRIPE-EXPRESS-CHECKOUT-NONCE' header using superglobals.
1662+
$nonce = sanitize_text_field( wp_unslash( $_SERVER['HTTP_X_WCSTRIPE_EXPRESS_CHECKOUT_NONCE'] ?? '' ) );
1663+
if ( ! wp_verify_nonce( $nonce, 'wc_store_api_express_checkout' ) ) {
1664+
return false;
1665+
}
1666+
1667+
return true;
1668+
}
1669+
1670+
/**
1671+
* Check if is request to the Store API.
1672+
*
1673+
* @return bool
1674+
*/
1675+
public function is_request_to_store_api() {
1676+
if ( empty( $GLOBALS['wp']->query_vars['rest_route'] ) ) {
1677+
return false;
1678+
}
1679+
return 0 === strpos( $GLOBALS['wp']->query_vars['rest_route'], '/wc/store/v1/checkout' );
1680+
}
16441681
}

readme.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,6 @@ If you get stuck, you can ask for help in the [Plugin Forum](https://wordpress.o
163163
* Fix - Fix payment processing for $0 subscription with recurring coupon
164164
* Dev - Add e2e tests to cover Affirm purchase flow
165165
* Fix - Add safety check when checking error object
166-
* Update - Include extension data from block checkout when submitting an express checkout order
166+
* Fix - Correctly handle countries without states when using the express payment methods
167167

168168
[See changelog for full details across versions](https://raw.githubusercontent.com/woocommerce/woocommerce-gateway-stripe/trunk/changelog.txt).
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
<?php
2+
3+
namespace WooCommerce\Stripe\Tests\PaymentMethods;
4+
5+
use WC_Stripe_Express_Checkout_Ajax_Handler;
6+
use WC_Stripe_Express_Checkout_Helper;
7+
use WC_Stripe_Helper;
8+
use WP_UnitTestCase;
9+
10+
/**
11+
* These tests make assertions against class WC_Stripe_Express_Checkout_Ajax_Handler.
12+
*
13+
* @package WooCommerce/Stripe/WC_Stripe_Express_Checkout_Ajax_Handler
14+
*
15+
* WC_Stripe_Express_Checkout_Ajax_Handler_Test class.
16+
*/
17+
class WC_Stripe_Express_Checkout_Ajax_Handler_Test extends WP_UnitTestCase {
18+
19+
/**
20+
* Express checkout helper instance.
21+
*
22+
* @var WC_Stripe_Express_Checkout_Helper
23+
*/
24+
private $express_checkout_helper;
25+
26+
/**
27+
* Ajax handler instance.
28+
*
29+
* @var WC_Stripe_Express_Checkout_Ajax_Handler
30+
*/
31+
private $ajax_handler;
32+
33+
public function set_up() {
34+
parent::set_up();
35+
36+
$stripe_settings = WC_Stripe_Helper::get_stripe_settings();
37+
$stripe_settings['enabled'] = 'yes';
38+
$stripe_settings['testmode'] = 'yes';
39+
$stripe_settings['test_publishable_key'] = 'pk_test_key';
40+
$stripe_settings['test_secret_key'] = 'sk_test_key';
41+
WC_Stripe_Helper::update_main_stripe_settings( $stripe_settings );
42+
43+
$this->express_checkout_helper = $this->getMockBuilder( WC_Stripe_Express_Checkout_Helper::class )
44+
->disableOriginalConstructor()
45+
->getMock();
46+
$this->ajax_handler = new WC_Stripe_Express_Checkout_Ajax_Handler( $this->express_checkout_helper );
47+
}
48+
49+
50+
/**
51+
* Test modify_country_locale_for_express_checkout method.
52+
*
53+
* @dataProvider provide_test_modify_country_locale_for_express_checkout
54+
*/
55+
public function test_modify_country_locale_for_express_checkout( $is_express_context, $base_locale, $expected_state_required ) {
56+
$this->express_checkout_helper->expects( $this->any() )
57+
->method( 'is_express_checkout_context' )
58+
->willReturn( $is_express_context );
59+
60+
$result = $this->ajax_handler->modify_country_locale_for_express_checkout( $base_locale );
61+
62+
$this->assertEquals( $expected_state_required, $result['AF']['state']['required'] );
63+
// Countries with states should remain unchanged.
64+
$this->assertTrue( $result['US']['state']['required'] );
65+
}
66+
67+
/**
68+
* Data provider for test_modify_country_locale_for_express_checkout.
69+
*
70+
* @return array
71+
*/
72+
public function provide_test_modify_country_locale_for_express_checkout() {
73+
$base_locale = [
74+
'US' => [
75+
'state' => [
76+
'required' => true,
77+
],
78+
],
79+
'GB' => [
80+
'state' => [
81+
'required' => true,
82+
],
83+
],
84+
'AF' => [
85+
'state' => [
86+
'required' => true,
87+
],
88+
],
89+
'RO' => [
90+
'state' => [
91+
'required' => true,
92+
],
93+
],
94+
];
95+
96+
return [
97+
'Not express checkout context - locale unchanged' => [
98+
'is_express_context' => false,
99+
'input_locale' => $base_locale,
100+
'expected_state_required' => true,
101+
],
102+
'Express checkout context - locale modified for countries without states' => [
103+
'is_express_context' => true,
104+
'input_locale' => $base_locale,
105+
'expected_state_required' => false,
106+
],
107+
];
108+
}
109+
}

0 commit comments

Comments
 (0)