From 01876f53446fa355c8b454c49342ab12e0140847 Mon Sep 17 00:00:00 2001 From: Geoff Taylor Date: Thu, 11 Jun 2026 20:02:26 -0400 Subject: [PATCH] feat: persist shipping phone through the checkout mutation (#1017) * feat: persist shipping phone through the checkout mutation The shipping fieldset in Checkout_Mutation::get_checkout_fields() omitted the phone field, so a phone passed in the checkout mutation's shipping input was never mapped into shipping_phone and WC_Order::set_shipping_phone() was never called. Add phone to the shipping fieldset so it flows through to the order, matching the billing fieldset. Closes #1016 * fix: load QL session handler when a session token header is present should_load_session_handler() only matched explicit wc-ajax / WC_DOING_AJAX and REST_REQUEST contexts. Headless callers drive session state through the Store-API Cart-Token header or the legacy woocommerce-session header and can land on other admin-ajax/REST entrypoints, leaving the session bootstrapped from an absent cookie. Detect either session header and also honor wp_doing_ajax() so QL_Session_Handler loads and rebuilds the session from the token. * ci: authenticate composer GitHub access to fix flaky strauss install The dependency-install steps run with the github-oauth token that composer rejects ("contains invalid characters"), so composer and strauss's internal composer bootstrap fall back to unauthenticated GitHub access. Under the shared runner IP's 60/hr unauthenticated rate limit, strauss intermittently exits 1 during post-install-cmd, failing lint, PHPStan, wpunit, functional and acceptance jobs at random. Provide a well-formed COMPOSER_AUTH built from the always-available GITHUB_TOKEN on every dependency-install step so GitHub API access is authenticated (5000/hr), removing the flake. * ci: run strauss with a clean composer auth context setup-php writes the runner's GITHUB_TOKEN into composer's global auth.json, and this composer rejects that token format ("github oauth token contains invalid characters"). strauss spins up its own internal Composer instance, which reads that auth, hits the validation error and exits 1 with no output, failing every job at the post-install/post-update strauss step. strauss only rewrites local vendor packages and needs no GitHub auth, so run it with COMPOSER_AUTH unset and a throwaway COMPOSER_HOME. Fixes all CI install steps, local installs and release packaging from one place. This reverts the earlier per-workflow COMPOSER_AUTH attempt, which could not work because composer rejects the token regardless of how it is supplied. * style: align equals sign in should_load_session_handler (phpcs) --- composer.json | 2 +- includes/class-woocommerce.php | 26 +++++++++++- .../data/mutation/class-checkout-mutation.php | 1 + tests/wpunit/CheckoutMutationTest.php | 42 +++++++++++++++++++ 4 files changed, 68 insertions(+), 3 deletions(-) diff --git a/composer.json b/composer.json index a6583c20..07e34300 100644 --- a/composer.json +++ b/composer.json @@ -75,7 +75,7 @@ "stan": "phpstan analyze --ansi --memory-limit=4G", "strauss": [ "test -f ./bin/strauss.phar || curl -o bin/strauss.phar -L -C - https://github.com/BrianHenryIE/strauss/releases/download/0.14.0/strauss.phar", - "@php bin/strauss.phar", + "env -u COMPOSER_AUTH COMPOSER_HOME=$(mktemp -d) php bin/strauss.phar", "composer dump-autoload --optimize" ], "cghooks": [ diff --git a/includes/class-woocommerce.php b/includes/class-woocommerce.php index 21ee1392..359e333e 100644 --- a/includes/class-woocommerce.php +++ b/includes/class-woocommerce.php @@ -124,14 +124,36 @@ class WooCommerce { * @return boolean */ public static function should_load_session_handler() { + // Any request carrying either the Store-API Cart-Token header or the + // legacy `woocommerce-session` (filterable) header is a headless + // caller driving session state through the token, regardless of + // which WP endpoint it lands on (admin-ajax, REST, the front-end, + // etc.). We need QL_Session_Handler here too so the session is + // bootstrapped from the token instead of the (absent) WC session + // cookie. + $legacy_header_key = 'HTTP_' . strtoupper( + preg_replace( + '#[^A-z0-9]#', + '_', + apply_filters( 'graphql_woocommerce_cart_session_http_header', 'woocommerce-session' ) + ) + ); + $has_session_header = ! empty( $_SERVER['HTTP_CART_TOKEN'] ) // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash + || ! empty( $_SERVER[ $legacy_header_key ] ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash + switch ( true ) { case \WPGraphQL\Router::is_graphql_http_request(): //phpcs:disable case 'on' === woographql_setting( 'enable_ql_session_handler_on_ajax', 'off' ) - && ( ! empty( $_GET['wc-ajax'] ) || defined( 'WC_DOING_AJAX' ) ): + && ( + ! empty( $_GET['wc-ajax'] ) + || defined( 'WC_DOING_AJAX' ) + || wp_doing_ajax() + || $has_session_header + ): //phpcs:enable case 'on' === woographql_setting( 'enable_ql_session_handler_on_rest', 'off' ) - && ( defined( 'REST_REQUEST' ) && REST_REQUEST ): + && ( ( defined( 'REST_REQUEST' ) && REST_REQUEST ) || $has_session_header ): return true; default: return false; diff --git a/includes/data/mutation/class-checkout-mutation.php b/includes/data/mutation/class-checkout-mutation.php index 8498543c..16ce19a9 100644 --- a/includes/data/mutation/class-checkout-mutation.php +++ b/includes/data/mutation/class-checkout-mutation.php @@ -164,6 +164,7 @@ class Checkout_Mutation { 'postcode' => 'postcode', 'state' => 'state', 'country' => 'country', + 'phone' => 'phone', ], 'account' => [ 'username' => 'username', diff --git a/tests/wpunit/CheckoutMutationTest.php b/tests/wpunit/CheckoutMutationTest.php index c22eb2d9..aaa1a5e6 100644 --- a/tests/wpunit/CheckoutMutationTest.php +++ b/tests/wpunit/CheckoutMutationTest.php @@ -133,6 +133,7 @@ class CheckoutMutationTest extends \Tests\WPGraphQL\WooCommerce\TestCase\WooGrap state postcode country + phone } paymentMethod paymentMethodTitle @@ -293,6 +294,7 @@ class CheckoutMutationTest extends \Tests\WPGraphQL\WooCommerce\TestCase\WooGrap 'state' => 'NY', 'postcode' => '12345', 'country' => 'US', + 'phone' => '555-555-6789', ], 'metaData' => [ [ @@ -446,6 +448,8 @@ class CheckoutMutationTest extends \Tests\WPGraphQL\WooCommerce\TestCase\WooGrap 'checkout.customer.id', $this->toRelayId( 'user', $this->customer ) ), + $this->expectedField( 'checkout.order.billing.phone', '555-555-1234' ), + $this->expectedField( 'checkout.order.shipping.phone', '555-555-6789' ), $this->expectedField( 'checkout.result', 'success' ), $this->expectedField( 'checkout.redirect', static::NOT_NULL ), ]; @@ -463,6 +467,44 @@ class CheckoutMutationTest extends \Tests\WPGraphQL\WooCommerce\TestCase\WooGrap $this->assertQuerySuccessful( $response, $expected ); } + public function testCheckoutMutationPersistsShippingPhone() { + $this->loginAsCustomer(); + + $product_id = $this->factory->product->createSimple(); + WC()->cart->add_to_cart( $product_id, 1 ); + + $input = [ + 'shipping' => [ + 'firstName' => 'May', + 'lastName' => 'Parker', + 'address1' => '20 Ingram St', + 'city' => 'New York City', + 'state' => 'NY', + 'postcode' => '12345', + 'country' => 'US', + 'phone' => '555-555-6789', + ], + ]; + $variables = [ 'input' => $this->getCheckoutInput( $input ) ]; + $query = $this->getCheckoutMutation(); + + $response = $this->graphql( compact( 'query', 'variables' ) ); + + // The shipping phone is returned on the order and is distinct from the billing phone. + $this->assertQuerySuccessful( + $response, + [ + $this->expectedField( 'checkout.order.billing.phone', '555-555-1234' ), + $this->expectedField( 'checkout.order.shipping.phone', '555-555-6789' ), + ] + ); + + // And it is saved on the underlying WC_Order object. + $order_id = $response['data']['checkout']['order']['databaseId']; + $order = \wc_get_order( $order_id ); + $this->assertEquals( '555-555-6789', $order->get_shipping_phone() ); + } + public function testCheckoutMutationWithNewAccount() { $variable = $this->factory->product_variation->createSome(); $product_ids = [