From ef53404fc38d50067457abb46e45745d7359b22b Mon Sep 17 00:00:00 2001 From: Geoff Taylor Date: Fri, 16 Jan 2026 12:28:23 -0500 Subject: [PATCH] fix: product variation duplicate fields removed (#957) * fix: product variation duplicate fields removed * chore: Linter compliances met * chore: Linter compliances met * chore: Linter compliances met * chore: Linter compliances met * chore: Linter compliances met * chore: Linter compliances met * devops: CI compliances met * devops: unneeded access function removed * devops: unneeded access function removed * devops: unneeded access function removed --- bin/entrypoint.sh | 13 ++ composer.lock | 12 +- docs/README.md | 70 ++++++ ...iguring-graphql-client-for-user-session.md | 4 +- docs/development-w-docker.md | 4 +- docs/handling-user-authentication.md | 4 +- ...g-user-session-and-using-cart-mutations.md | 4 +- docs/harmonizing-with-wordpress.md | 4 +- docs/installation.md | 4 +- docs/local-testing.md | 6 +- docs/routing-by-uri.md | 4 +- docs/settings.md | 4 +- docs/testing-quick-start.md | 4 +- docs/testing-w-docker.md | 4 +- docs/toc.md | 5 +- docs/using-cart-data.md | 4 +- ...g-checkout-mutation-and-order-mutations.md | 4 +- ...ng-composite-product-data-and-mutations.md | 4 +- docs/using-customer-data-and-mutations.md | 4 +- docs/using-order-data.md | 4 +- ...using-product-addons-data-and-mutations.md | 4 +- ...using-product-bundle-data-and-mutations.md | 4 +- docs/using-product-data.md | 4 +- docs/using-subscription-data-and-mutations.md | 4 +- includes/class-jwt-auth-schema-filters.php | 57 +++-- includes/class-type-registry.php | 2 +- includes/class-woocommerce-filters.php | 29 ++- includes/class-wp-graphql-woocommerce.php | 2 +- includes/connection/class-orders.php | 201 +++++++++--------- includes/model/class-customer.php | 4 +- .../class-payment-method-set-default.php | 2 +- ....php => class-payment-token-interface.php} | 33 +-- .../interface/class-product-variation.php | 199 +---------------- includes/type/interface/class-product.php | 3 - includes/type/object/class-customer-type.php | 169 +++++++++++---- .../type/object/class-order-item-type.php | 3 +- .../type/object/class-payment-token-types.php | 22 +- .../type/object/class-shipping-rate-type.php | 4 +- includes/utils/class-ql-session-handler.php | 38 +++- tests/_support/Helper/GraphQLE2E.php | 8 + .../ReturningCustomerSessionCept.php | 11 +- tests/functional/QLSessionHandlerCest.php | 5 +- tests/wpunit/CustomerQueriesTest.php | 18 -- tests/wpunit/PaymentMethodMutationsTest.php | 12 +- wp-graphql-woocommerce.php | 22 ++ 45 files changed, 526 insertions(+), 500 deletions(-) create mode 100644 docs/README.md rename includes/type/interface/{class-payment-token.php => class-payment-token-interface.php} (65%) diff --git a/bin/entrypoint.sh b/bin/entrypoint.sh index 6aaa0246..3a04489a 100755 --- a/bin/entrypoint.sh +++ b/bin/entrypoint.sh @@ -52,6 +52,19 @@ wp config create \ --quiet \ --allow-root +# Install WP if not yet installed +if ! $( wp core is-installed --allow-root ); then + echo "Installing WordPress..." + wp core install \ + --path="${WP_ROOT_FOLDER}" \ + --url="${WORDPRESS_URL}" \ + --title='Test' \ + --admin_user="${ADMIN_USERNAME}" \ + --admin_password="${ADMIN_PASSWORD}" \ + --admin_email="${ADMIN_EMAIL}" \ + --allow-root +fi + # Use alternative database export for WordPress 6.8+ to avoid MariaDB SSL issues if [[ "${WP_VERSION}" == "6.8"* ]]; then echo "Using alternative database export method for WordPress 6.8+" diff --git a/composer.lock b/composer.lock index b7416220..85fbb44a 100644 --- a/composer.lock +++ b/composer.lock @@ -2046,16 +2046,16 @@ }, { "name": "symfony/string", - "version": "v6.4.26", + "version": "v6.4.30", "source": { "type": "git", "url": "https://github.com/symfony/string.git", - "reference": "5621f039a71a11c87c106c1c598bdcd04a19aeea" + "reference": "50590a057841fa6bf69d12eceffce3465b9e32cb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/string/zipball/5621f039a71a11c87c106c1c598bdcd04a19aeea", - "reference": "5621f039a71a11c87c106c1c598bdcd04a19aeea", + "url": "https://api.github.com/repos/symfony/string/zipball/50590a057841fa6bf69d12eceffce3465b9e32cb", + "reference": "50590a057841fa6bf69d12eceffce3465b9e32cb", "shasum": "" }, "require": { @@ -2111,7 +2111,7 @@ "utf8" ], "support": { - "source": "https://github.com/symfony/string/tree/v6.4.26" + "source": "https://github.com/symfony/string/tree/v6.4.30" }, "funding": [ { @@ -2131,7 +2131,7 @@ "type": "tidelift" } ], - "time": "2025-09-11T14:32:46+00:00" + "time": "2025-11-21T18:03:05+00:00" }, { "name": "szepeviktor/phpstan-wordpress", diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 00000000..73ea3f91 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,70 @@ + + +# WPGraphQL for WooCommerce + +WPGraphQL for WooCommerce (WooGraphQL) is a free, open-source WordPress plugin that extends the WPGraphQL plugin, allowing you to access WooCommerce data through GraphQL queries and mutations. + +## Table of Contents + +### Getting Started + +- [Installation](./installation.md) +- [Settings](./settings.md) +- [Configuring GraphQL Client For the User Session](./configuring-graphql-client-for-user-session.md) +- [Handling User Authentication](./handling-user-authentication.md) + +### WooGraphQL + +- [Routing by URI](./routing-by-uri.md) +- [Using Product Data](./using-product-data.md) +- [Creating Session Provider and using Cart Mutations](./handling-user-session-and-using-cart-mutations.md) +- [Using Cart Data](./using-cart-data.md) +- [Harmonizing with WordPress](./harmonizing-with-wordpress.md) +- [Using Checkout Mutation + Order Mutations](./using-checkout-mutation-and-order-mutations.md) +- [Using Order Data](./using-order-data.md) +- [Using Customer Data + Mutations](./using-customer-data-and-mutations.md) + +### WooGraphQL Pro + +For premium WooCommerce extension support (Subscriptions, Composite Products, Bundles, Add-ons), see the [WooGraphQL Pro Documentation](https://woographql.com/docs/woographql-pro). + +### Contributing + +- [Testing Quick-Start Guide](./testing-quick-start.md) +- [Local CLI Testing](./local-testing.md) +- [Development with Docker](./development-w-docker.md) +- [CLI Testing with Docker](./testing-w-docker.md) + +## Introduction + +WooGraphQL brings the power of GraphQL to WooCommerce, enabling developers to build modern, performant headless e-commerce applications. With WooGraphQL, you can: + +- Query products, categories, tags, and attributes +- Manage shopping carts with session-based mutations +- Handle user authentication and customer accounts +- Process checkouts and manage orders +- Access WooCommerce settings and configurations + +Whether you're building a React, Vue, or any other frontend application, WooGraphQL provides a flexible and efficient API to interact with your WooCommerce store. + +### Key Features + +- **Complete WooCommerce Integration**: Access all WooCommerce data types including products, orders, customers, coupons, and more +- **Session Management**: Built-in support for guest and authenticated user sessions +- **Cart Operations**: Full cart functionality with add, remove, update, and clear operations +- **Checkout Process**: Complete checkout mutation with payment gateway integration +- **Extensible**: Hooks and filters to customize the schema and responses + +### Requirements + +- WordPress 5.6+ +- WooCommerce 6.0+ +- WPGraphQL 1.14+ +- PHP 7.4+ + +Get started by following the [Installation Guide](./installation.md). diff --git a/docs/configuring-graphql-client-for-user-session.md b/docs/configuring-graphql-client-for-user-session.md index ab141f75..6b2af5bf 100644 --- a/docs/configuring-graphql-client-for-user-session.md +++ b/docs/configuring-graphql-client-for-user-session.md @@ -1,9 +1,9 @@ ---- + # Configuring a GraphQL Client for WooCommerce User Session Management diff --git a/docs/development-w-docker.md b/docs/development-w-docker.md index a8e75ea1..71bd02fe 100644 --- a/docs/development-w-docker.md +++ b/docs/development-w-docker.md @@ -1,9 +1,9 @@ ---- + # Coming Soon diff --git a/docs/handling-user-authentication.md b/docs/handling-user-authentication.md index 56a2deb3..7ce0faa2 100644 --- a/docs/handling-user-authentication.md +++ b/docs/handling-user-authentication.md @@ -1,9 +1,9 @@ ---- + # Handling User Authentication diff --git a/docs/handling-user-session-and-using-cart-mutations.md b/docs/handling-user-session-and-using-cart-mutations.md index 096a3d51..96e22fc6 100644 --- a/docs/handling-user-session-and-using-cart-mutations.md +++ b/docs/handling-user-session-and-using-cart-mutations.md @@ -1,9 +1,9 @@ ---- + # Handling User Session and Using Cart Mutations diff --git a/docs/harmonizing-with-wordpress.md b/docs/harmonizing-with-wordpress.md index 93ecdc85..5991cf8e 100644 --- a/docs/harmonizing-with-wordpress.md +++ b/docs/harmonizing-with-wordpress.md @@ -1,9 +1,9 @@ ---- + # Harmonizing with WordPress diff --git a/docs/installation.md b/docs/installation.md index 1f53a204..4a68c7f6 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -1,9 +1,9 @@ ---- + # Installation diff --git a/docs/local-testing.md b/docs/local-testing.md index e3cd373a..edec09f1 100644 --- a/docs/local-testing.md +++ b/docs/local-testing.md @@ -1,9 +1,9 @@ ---- + # Local CLI Testing diff --git a/docs/routing-by-uri.md b/docs/routing-by-uri.md index 50a4fd27..de002f6b 100644 --- a/docs/routing-by-uri.md +++ b/docs/routing-by-uri.md @@ -1,9 +1,9 @@ ---- + # Routing By URI diff --git a/docs/settings.md b/docs/settings.md index d8aca857..73ed2370 100644 --- a/docs/settings.md +++ b/docs/settings.md @@ -1,9 +1,9 @@ ---- + # WPGraphQL for WooCommerce Settings diff --git a/docs/testing-quick-start.md b/docs/testing-quick-start.md index 79e2e102..5d47bbcc 100644 --- a/docs/testing-quick-start.md +++ b/docs/testing-quick-start.md @@ -1,9 +1,9 @@ ---- + # Testing (Quick-Start Guide) diff --git a/docs/testing-w-docker.md b/docs/testing-w-docker.md index 1c9cf1d4..4b0502d5 100644 --- a/docs/testing-w-docker.md +++ b/docs/testing-w-docker.md @@ -1,9 +1,9 @@ ---- + # Coming Soon diff --git a/docs/toc.md b/docs/toc.md index 45d44cf2..82a1f4d0 100644 --- a/docs/toc.md +++ b/docs/toc.md @@ -20,10 +20,7 @@ ## WooGraphQL Pro -- [Using Subscription Data + Mutations](using-subscription-data-and-mutations.md) -- [Using Composite Product Data + Mutations](using-composite-product-data-and-mutations.md) -- [Using Product Bundle Data + Mutations](using-product-bundle-data-and-mutations.md) -- [Using Product Add-ons Data + Mutations](using-product-addons-data-and-mutations.md) +For documentation on using premium WooCommerce extensions with WooGraphQL, see the [WooGraphQL Pro Documentation](https://woographql.com/docs/woographql-pro). ## Extending WooGraphQL diff --git a/docs/using-cart-data.md b/docs/using-cart-data.md index c269f356..4dcf14c7 100644 --- a/docs/using-cart-data.md +++ b/docs/using-cart-data.md @@ -1,9 +1,9 @@ ---- + # Using Cart Data diff --git a/docs/using-checkout-mutation-and-order-mutations.md b/docs/using-checkout-mutation-and-order-mutations.md index 6478ce20..8850bb0d 100644 --- a/docs/using-checkout-mutation-and-order-mutations.md +++ b/docs/using-checkout-mutation-and-order-mutations.md @@ -1,9 +1,9 @@ ---- + # Using Checkout Mutation and Order Mutations diff --git a/docs/using-composite-product-data-and-mutations.md b/docs/using-composite-product-data-and-mutations.md index 7006c321..b2258f4f 100644 --- a/docs/using-composite-product-data-and-mutations.md +++ b/docs/using-composite-product-data-and-mutations.md @@ -1,9 +1,9 @@ ---- + # Using Composite Product Data + Mutations diff --git a/docs/using-customer-data-and-mutations.md b/docs/using-customer-data-and-mutations.md index 27f1579a..b61577a9 100644 --- a/docs/using-customer-data-and-mutations.md +++ b/docs/using-customer-data-and-mutations.md @@ -1,9 +1,9 @@ ---- + # Using Customer Data + Mutations diff --git a/docs/using-order-data.md b/docs/using-order-data.md index 9da1e90c..69af2292 100644 --- a/docs/using-order-data.md +++ b/docs/using-order-data.md @@ -1,9 +1,9 @@ ---- + # Using Order Data diff --git a/docs/using-product-addons-data-and-mutations.md b/docs/using-product-addons-data-and-mutations.md index e93832a9..5941a222 100644 --- a/docs/using-product-addons-data-and-mutations.md +++ b/docs/using-product-addons-data-and-mutations.md @@ -1,9 +1,9 @@ ---- + # Coming Soon diff --git a/docs/using-product-bundle-data-and-mutations.md b/docs/using-product-bundle-data-and-mutations.md index 9b004b30..4ab7ca87 100644 --- a/docs/using-product-bundle-data-and-mutations.md +++ b/docs/using-product-bundle-data-and-mutations.md @@ -1,9 +1,9 @@ ---- + # Using Product Bundle Data + Mutations diff --git a/docs/using-product-data.md b/docs/using-product-data.md index 9dcf1184..3ade7789 100644 --- a/docs/using-product-data.md +++ b/docs/using-product-data.md @@ -1,9 +1,9 @@ ---- + # Using Product Data diff --git a/docs/using-subscription-data-and-mutations.md b/docs/using-subscription-data-and-mutations.md index 9907cbca..063f024c 100644 --- a/docs/using-subscription-data-and-mutations.md +++ b/docs/using-subscription-data-and-mutations.md @@ -1,9 +1,9 @@ ---- + # Using Subscription Data + Mutations diff --git a/includes/class-jwt-auth-schema-filters.php b/includes/class-jwt-auth-schema-filters.php index 763be8a9..4ab3b0ce 100644 --- a/includes/class-jwt-auth-schema-filters.php +++ b/includes/class-jwt-auth-schema-filters.php @@ -188,24 +188,47 @@ class JWT_Auth_Schema_Filters { ); if ( ! WooCommerce_Filters::is_session_handler_disabled() ) { - register_graphql_field( - 'LoginPayload', - 'sessionToken', - [ - 'type' => 'String', - 'description' => __( 'A JWT token that can be used in future requests to for WooCommerce session identification', 'wp-graphql-woocommerce' ), - 'resolve' => static function () { - /** - * Session Handler. - * - * @var \WPGraphQL\WooCommerce\Utils\QL_Session_Handler $session - */ - $session = \WC()->session; + $token_type = woographql_setting( 'set_session_token_type', 'legacy' ); + if ( in_array( $token_type, [ 'legacy', 'both' ], true ) ) { + register_graphql_field( + 'LoginPayload', + 'sessionToken', + [ + 'type' => 'String', + 'description' => __( 'A JWT token that can be used in future requests to for WooCommerce session identification', 'wp-graphql-woocommerce' ), + 'resolve' => static function () { + /** + * Session Handler. + * + * @var \WPGraphQL\WooCommerce\Utils\QL_Session_Handler $session + */ + $session = \WC()->session; - return apply_filters( 'graphql_customer_session_token', $session->build_token() ); - }, - ] - ); + return apply_filters( 'graphql_customer_session_token', $session->build_token() ); + }, + ] + ); + } + if ( in_array( $token_type, [ 'store-api', 'both' ], true ) ) { + register_graphql_field( + 'LoginPayload', + 'cartToken', + [ + 'type' => 'String', + 'description' => __( 'A JWT token that can be used in future requests to for WooCommerce session identification', 'wp-graphql-woocommerce' ), + 'resolve' => static function () { + /** + * Session Handler. + * + * @var \WPGraphQL\WooCommerce\Utils\QL_Session_Handler $session + */ + $session = \WC()->session; + + return apply_filters( 'graphql_customer_session_token', $session->build_cart_token() ); + }, + ] + ); + } } } } diff --git a/includes/class-type-registry.php b/includes/class-type-registry.php index eb4f9ad1..328a87fc 100644 --- a/includes/class-type-registry.php +++ b/includes/class-type-registry.php @@ -81,7 +81,7 @@ class Type_Registry { Type\WPInterface\Attribute::register_interface(); Type\WPInterface\Product_Attribute::register_interface(); Type\WPInterface\Cart_Error::register_interface(); - Type\WPInterface\Payment_Token::register_interface(); + Type\WPInterface\Payment_Token_Interface::register_interface(); Type\WPInterface\Product_Union::register_interface(); Type\WPInterface\Cart_Item::register_interface(); Type\WPInterface\Downloadable_Product::register_interface(); diff --git a/includes/class-woocommerce-filters.php b/includes/class-woocommerce-filters.php index 60ff7682..87b96ffd 100644 --- a/includes/class-woocommerce-filters.php +++ b/includes/class-woocommerce-filters.php @@ -34,6 +34,9 @@ class WooCommerce_Filters { add_filter( 'woocommerce_session_handler', [ self::class, 'woocommerce_session_handler' ] ); add_filter( 'graphql_response_headers_to_send', [ self::class, 'add_session_header_to_expose_headers' ] ); add_filter( 'graphql_access_control_allow_headers', [ self::class, 'add_session_header_to_allow_headers' ] ); + + // Initialize cart/session after JWT authentication has had a chance to run. + add_action( 'graphql_process_http_request', [ self::class, 'initialize_session_and_cart' ] ); } // Add better support for Stripe payment gateway. @@ -50,7 +53,31 @@ class WooCommerce_Filters { * @return boolean */ public static function is_session_handler_disabled() { - return defined( 'NO_QL_SESSION_HANDLER' ) || 'on' === woographql_setting( 'disable_ql_session_handler', 'off' ); + return \defined( 'NO_QL_SESSION_HANDLER' ) || 'on' === woographql_setting( 'disable_ql_session_handler', 'off' ); + } + + /** + * Initialize WooCommerce session and cart for GraphQL requests. + * + * This is hooked to 'graphql_before_execute' to ensure JWT authentication has + * had a chance to set the current user before the session is initialized. + * This fixes an issue where guest sessions weren't being updated when a user + * provides both a Cart-Token (session) and Authorization (JWT) header. + * + * @return void + */ + public static function initialize_session_and_cart() { + // Clear any existing WooCommerce objects to ensure fresh initialization + // with the correct user context after JWT authentication. + + // @phpstan-ignore-next-line + \WC()->customer = null; + // @phpstan-ignore-next-line + \WC()->cart = null; + // @phpstan-ignore-next-line + \WC()->session = null; + + wc_load_cart(); } /** diff --git a/includes/class-wp-graphql-woocommerce.php b/includes/class-wp-graphql-woocommerce.php index 2f06fa53..9759a6be 100644 --- a/includes/class-wp-graphql-woocommerce.php +++ b/includes/class-wp-graphql-woocommerce.php @@ -261,7 +261,7 @@ if ( ! class_exists( '\WPGraphQL\WooCommerce\WP_GraphQL_WooCommerce' ) ) : require $include_directory_path . 'type/interface/class-product-attribute.php'; require $include_directory_path . 'type/interface/class-product.php'; require $include_directory_path . 'type/interface/class-product-variation.php'; - require $include_directory_path . 'type/interface/class-payment-token.php'; + require $include_directory_path . 'type/interface/class-payment-token-interface.php'; require $include_directory_path . 'type/interface/class-product-union.php'; require $include_directory_path . 'type/interface/class-cart-item.php'; require $include_directory_path . 'type/interface/class-downloadable-product.php'; diff --git a/includes/connection/class-orders.php b/includes/connection/class-orders.php index c270c33f..835bc59d 100644 --- a/includes/connection/class-orders.php +++ b/includes/connection/class-orders.php @@ -12,6 +12,7 @@ namespace WPGraphQL\WooCommerce\Connection; use GraphQL\Type\Definition\ResolveInfo; use WPGraphQL\AppContext; use WPGraphQL\WooCommerce\Data\Connection\Order_Connection_Resolver; +use WPGraphQL\WooCommerce\Model\Customer; /** * Class - Orders @@ -93,104 +94,6 @@ class Orders { ); } - /** - * Returns order connection filter by customer. - * - * @param \WPGraphQL\WooCommerce\Data\Connection\Order_Connection_Resolver $resolver Connection resolver. - * @param \WC_Customer $customer Customer object of querying user. - * - * @return array|\GraphQL\Deferred - */ - private static function get_customer_order_connection( $resolver, $customer ) { - // If not "billing email" or "ID" set bail early by returning an empty connection. - if ( empty( $customer->get_billing_email() ) && empty( $customer->get_id() ) ) { - return [ - 'nodes' => [], - 'edges' => [], - ]; - } - - $customer_id = $customer->get_id(); - $billing_email = $customer->get_billing_email(); - if ( ! empty( $customer_id ) ) { - $resolver->set_query_arg( 'customer_id', $customer_id ); - $resolver->set_should_execute( \WC()->customer->get_id() === $customer_id ); - } elseif ( ! empty( $billing_email ) ) { - $resolver->set_query_arg( 'billing_email', $billing_email ); - $resolver->set_should_execute( \WC()->customer->get_billing_email() === $billing_email ); - } - - return $resolver->get_connection(); - } - - /** - * Returns refund connection filter by customer. - * - * @param \WPGraphQL\WooCommerce\Data\Connection\Order_Connection_Resolver $resolver Connection resolver. - * @param \WC_Customer $customer Customer object of querying user. - * - * @return array|\GraphQL\Deferred - */ - private static function get_customer_refund_connection( $resolver, $customer ) { - $empty_results = [ - 'pageInfo' => null, - 'nodes' => [], - 'edges' => [], - ]; - // If not "billing email" or "ID" set bail early by returning an empty connection. - if ( empty( $customer->get_billing_email() ) && empty( $customer->get_id() ) ) { - return $empty_results; - } - - $order_ids = []; - $customer_id = $customer->get_id(); - $billing_email = $customer->get_billing_email(); - if ( ! empty( $customer_id ) ) { - $args = [ - 'customer_id' => $customer_id, - 'return' => 'ids', - ]; - /** @var array $order_ids_by_customer_id */ - $order_ids_by_customer_id = wc_get_orders( $args ); - - if ( is_array( $order_ids_by_customer_id ) ) { - $order_ids = $order_ids_by_customer_id; - } - } - - if ( ! empty( $billing_email ) ) { - $args = [ - 'billing_email' => $billing_email, - 'return' => 'ids', - ]; - /** @var array $order_ids_by_email */ - $order_ids_by_email = wc_get_orders( $args ); - // Merge the arrays of order IDs. - if ( is_array( $order_ids_by_email ) ) { - $order_ids = array_merge( $order_ids, $order_ids_by_email ); - } - } - - // If no orders found, return empty connection. - if ( empty( $order_ids ) ) { - return $empty_results; - } - - // Remove duplicates. - $order_ids = array_unique( $order_ids ); - - // Set connection args. - $resolver->set_should_execute( - ( 0 !== $customer_id && \WC()->customer->get_id() === $customer_id ) - || \WC()->customer->get_billing_email() === $billing_email - ); - - $resolver->set_query_arg( 'post_parent__in', array_map( 'absint', $order_ids ) ); - - // Execute and return connection. - return $resolver->get_connection(); - } - /** * Given an array of $args, this returns the connection config, merging the provided args * with the defaults. @@ -233,8 +136,8 @@ class Orders { */ if ( $not_manager ) { return 'shop_order_refund' === $post_object->name - ? self::get_customer_refund_connection( $resolver, \WC()->customer ) - : self::get_customer_order_connection( $resolver, \WC()->customer ); + ? self::get_customer_refund_connection( $resolver, new Customer( 'session', ! is_user_logged_in() ) ) + : self::get_customer_order_connection( $resolver, new Customer( 'session', ! is_user_logged_in() ) ); } return $resolver->get_connection(); @@ -330,4 +233,102 @@ class Orders { ] ); } + + /** + * Returns order connection filter by customer. + * + * @param \WPGraphQL\WooCommerce\Data\Connection\Order_Connection_Resolver $resolver Connection resolver. + * @param \WPGraphQL\WooCommerce\Model\Customer $customer Customer object of querying user. + * + * @return array|\GraphQL\Deferred + */ + private static function get_customer_order_connection( $resolver, Customer $customer ) { + // If not "billing email" or "ID" set bail early by returning an empty connection. + if ( empty( $customer->billing['email'] ) && ( empty( absint( $customer->ID ) ) ) ) { + return [ + 'nodes' => [], + 'edges' => [], + ]; + } + + $target_customer_id = absint( $customer->ID ); + $current_customer_id = absint( \WC()->customer->get_id() ); + if ( ! empty( $target_customer_id ) ) { + $resolver->set_query_arg( 'customer_id', $target_customer_id ); + $resolver->set_should_execute( $current_customer_id === $target_customer_id ); + } elseif ( ! empty( $customer->billing['email'] ) ) { + $resolver->set_query_arg( 'billing_email', $customer->billing['email'] ); + $resolver->set_should_execute( \WC()->customer->get_billing_email() === $customer->billing['email'] ); + } + + return $resolver->get_connection(); + } + + /** + * Returns refund connection filter by customer. + * + * @param \WPGraphQL\WooCommerce\Data\Connection\Order_Connection_Resolver $resolver Connection resolver. + * @param \WPGraphQL\WooCommerce\Model\Customer $customer Customer object of querying user. + * + * @return array|\GraphQL\Deferred + */ + private static function get_customer_refund_connection( Order_Connection_Resolver $resolver, Customer $customer ) { + $empty_results = [ + 'pageInfo' => null, + 'nodes' => [], + 'edges' => [], + ]; + // If not "billing email" or "ID" set bail early by returning an empty connection. + if ( empty( $customer->billing['email'] ) && empty( $customer->ID ) ) { + return $empty_results; + } + + $order_ids = []; + $customer_id = $customer->ID; + $billing_email = $customer->billing['email']; + if ( ! empty( $customer_id ) ) { + $args = [ + 'customer_id' => $customer_id, + 'return' => 'ids', + ]; + /** @var array $order_ids_by_customer_id */ + $order_ids_by_customer_id = wc_get_orders( $args ); + + if ( is_array( $order_ids_by_customer_id ) ) { + $order_ids = $order_ids_by_customer_id; + } + } + + if ( ! empty( $billing_email ) ) { + $args = [ + 'billing_email' => $billing_email, + 'return' => 'ids', + ]; + /** @var array $order_ids_by_email */ + $order_ids_by_email = wc_get_orders( $args ); + // Merge the arrays of order IDs. + if ( is_array( $order_ids_by_email ) ) { + $order_ids = array_merge( $order_ids, $order_ids_by_email ); + } + } + + // If no orders found, return empty connection. + if ( empty( $order_ids ) ) { + return $empty_results; + } + + // Remove duplicates. + $order_ids = array_unique( $order_ids ); + + // Set connection args. + $resolver->set_should_execute( + ( 0 !== $customer_id && \WC()->customer->get_id() === $customer_id ) + || \WC()->customer->get_billing_email() === $billing_email + ); + + $resolver->set_query_arg( 'post_parent__in', array_map( 'absint', $order_ids ) ); + + // Execute and return connection. + return $resolver->get_connection(); + } } diff --git a/includes/model/class-customer.php b/includes/model/class-customer.php index 6abe9c83..51156dbc 100644 --- a/includes/model/class-customer.php +++ b/includes/model/class-customer.php @@ -17,8 +17,6 @@ use WPGraphQL\Model\Model; /** * Class Customer * - * @property \WC_Customer $wc_data - * * @property int $ID * @property string $id * @property int $databaseId @@ -86,7 +84,7 @@ class Customer extends Model { return ( ! empty( $this->data->get_id() ) ) ? $this->data->get_id() : \WC()->session->get_customer_id(); }, 'id' => function () { - return ( ! empty( $this->data->get_id() ) ) + return ! empty( $this->data->get_id() ) ? Relay::toGlobalId( 'user', $this->data->get_id() ) : 'guest'; }, diff --git a/includes/mutation/class-payment-method-set-default.php b/includes/mutation/class-payment-method-set-default.php index 6b77d2e0..ec0f905f 100644 --- a/includes/mutation/class-payment-method-set-default.php +++ b/includes/mutation/class-payment-method-set-default.php @@ -62,7 +62,7 @@ class Payment_Method_Set_Default { }, ], 'token' => [ - 'type' => 'PaymentToken', + 'type' => 'PaymentTokenInterface', 'description' => __( 'Preferred payment method token', 'wp-graphql-woocommerce' ), 'resolve' => static function ( $payload ) { return ! empty( $payload['token'] ) ? $payload['token'] : null; diff --git a/includes/type/interface/class-payment-token.php b/includes/type/interface/class-payment-token-interface.php similarity index 65% rename from includes/type/interface/class-payment-token.php rename to includes/type/interface/class-payment-token-interface.php index ca35e1be..d82b41f3 100644 --- a/includes/type/interface/class-payment-token.php +++ b/includes/type/interface/class-payment-token-interface.php @@ -1,6 +1,6 @@ __( 'Payment token object', 'wp-graphql-woocommerce' ), 'interfaces' => [ 'Node' ], @@ -36,13 +33,7 @@ class Payment_Token { case 'eCheck': return $type_registry->get_type( 'PaymentTokenECheck' ); default: - throw new UserError( - sprintf( - /* translators: %s: Payment token type */ - __( 'The "%s" token type is not supported by the core WPGraphQL for WooCommerce (WooGraphQL) schema.', 'wp-graphql-woocommerce' ), - $type - ) - ); + return $type_registry->get_type( 'PaymentToken' ); } }, ] @@ -58,20 +49,6 @@ class Payment_Token { public static function get_fields( $other_fields = [] ) { return array_merge( [ - 'id' => [ - 'type' => [ 'non_null' => 'ID' ], - 'description' => __( 'Token ID unique identifier', 'wp-graphql-woocommerce' ), - 'resolve' => static function ( $source ) { - return ! empty( $source->get_id() ) ? Relay::toGlobalId( 'token', $source->get_id() ) : null; - }, - ], - 'tokenId' => [ - 'type' => [ 'non_null' => 'Integer' ], - 'description' => __( 'Token database ID.', 'wp-graphql-woocommerce' ), - 'resolve' => static function ( $source ) { - return ! empty( $source->get_id() ) ? $source->get_id() : null; - }, - ], 'type' => [ 'type' => [ 'non_null' => 'String' ], 'description' => __( 'Token type', 'wp-graphql-woocommerce' ), diff --git a/includes/type/interface/class-product-variation.php b/includes/type/interface/class-product-variation.php index 3ae0228d..8c905816 100644 --- a/includes/type/interface/class-product-variation.php +++ b/includes/type/interface/class-product-variation.php @@ -12,7 +12,6 @@ use GraphQL\Type\Definition\ResolveInfo; use WPGraphQL\AppContext; use WPGraphQL\WooCommerce\Data\Connection\Product_Connection_Resolver; use WPGraphQL\WooCommerce\Data\Connection\Variation_Attribute_Connection_Resolver; -use WPGraphQL\WooCommerce\Type\WPObject\Meta_Data_Type; /** @@ -78,208 +77,14 @@ class Product_Variation { */ public static function get_fields() { return [ - 'id' => [ - 'type' => [ 'non_null' => 'ID' ], - 'description' => __( 'Product or variation global ID', 'wp-graphql-woocommerce' ), - ], - 'databaseId' => [ - 'type' => [ 'non_null' => 'Int' ], - 'description' => __( 'Product or variation ID', 'wp-graphql-woocommerce' ), - ], - 'name' => [ - 'type' => 'String', - 'description' => __( 'Product name', 'wp-graphql-woocommerce' ), - ], - 'date' => [ - 'type' => 'String', - 'description' => __( 'Date variation created', 'wp-graphql-woocommerce' ), - ], - 'modified' => [ - 'type' => 'String', - 'description' => __( 'Date variation last updated', 'wp-graphql-woocommerce' ), - ], - 'description' => [ - 'type' => 'String', - 'description' => __( 'Product description', 'wp-graphql-woocommerce' ), - ], - 'sku' => [ - 'type' => 'String', - 'description' => __( 'Product variation SKU (Stock-keeping unit)', 'wp-graphql-woocommerce' ), - ], - 'price' => [ - 'type' => 'String', - 'description' => __( 'Product variation\'s active price', 'wp-graphql-woocommerce' ), - 'args' => [ - 'format' => [ - 'type' => 'PricingFieldFormatEnum', - 'description' => __( 'Format of the price', 'wp-graphql-woocommerce' ), - ], - ], - 'resolve' => static function ( $source, $args ) { - if ( isset( $args['format'] ) && 'raw' === $args['format'] ) { - // @codingStandardsIgnoreLine. - return $source->priceRaw; - } else { - return $source->price; - } - }, - ], - 'regularPrice' => [ - 'type' => 'String', - 'description' => __( 'Product variation\'s regular price', 'wp-graphql-woocommerce' ), - 'args' => [ - 'format' => [ - 'type' => 'PricingFieldFormatEnum', - 'description' => __( 'Format of the price', 'wp-graphql-woocommerce' ), - ], - ], - 'resolve' => static function ( $source, $args ) { - if ( isset( $args['format'] ) && 'raw' === $args['format'] ) { - // @codingStandardsIgnoreLine. - return $source->regularPriceRaw; - } else { - // @codingStandardsIgnoreLine. - return $source->regularPrice; - } - }, - ], - 'salePrice' => [ - 'type' => 'String', - 'description' => __( 'Product variation\'s sale price', 'wp-graphql-woocommerce' ), - 'args' => [ - 'format' => [ - 'type' => 'PricingFieldFormatEnum', - 'description' => __( 'Format of the price', 'wp-graphql-woocommerce' ), - ], - ], - 'resolve' => static function ( $source, $args ) { - if ( isset( $args['format'] ) && 'raw' === $args['format'] ) { - // @codingStandardsIgnoreLine. - return $source->salePriceRaw; - } else { - // @codingStandardsIgnoreLine. - return $source->salePrice; - } - }, - ], - 'dateOnSaleFrom' => [ - 'type' => 'String', - 'description' => __( 'Date on sale from', 'wp-graphql-woocommerce' ), - ], - 'dateOnSaleTo' => [ - 'type' => 'String', - 'description' => __( 'Date on sale to', 'wp-graphql-woocommerce' ), - ], - 'onSale' => [ - 'type' => 'Boolean', - 'description' => __( 'Is variation on sale?', 'wp-graphql-woocommerce' ), - ], - 'status' => [ - 'type' => 'String', - 'description' => __( 'Variation status', 'wp-graphql-woocommerce' ), - ], - 'purchasable' => [ - 'type' => 'Boolean', - 'description' => __( 'If product variation can be bought', 'wp-graphql-woocommerce' ), - ], - 'virtual' => [ - 'type' => 'Boolean', - 'description' => __( 'Is product virtual?', 'wp-graphql-woocommerce' ), - ], - 'downloadable' => [ - 'type' => 'Boolean', - 'description' => __( 'Is downloadable?', 'wp-graphql-woocommerce' ), - ], - 'downloads' => [ - 'type' => [ 'list_of' => 'ProductDownload' ], - 'description' => __( 'Product downloads', 'wp-graphql-woocommerce' ), - ], - 'downloadLimit' => [ - 'type' => 'Int', - 'description' => __( 'Download limit', 'wp-graphql-woocommerce' ), - ], - 'downloadExpiry' => [ - 'type' => 'Int', - 'description' => __( 'Download expiry', 'wp-graphql-woocommerce' ), - ], - 'taxStatus' => [ - 'type' => 'TaxStatusEnum', - 'description' => __( 'Tax status', 'wp-graphql-woocommerce' ), - ], - 'taxClass' => [ - 'type' => 'TaxClassEnum', - 'description' => __( 'Product variation tax class', 'wp-graphql-woocommerce' ), - ], - 'manageStock' => [ - 'type' => 'ManageStockEnum', - 'description' => __( 'if/how product variation stock is managed', 'wp-graphql-woocommerce' ), - ], - 'stockQuantity' => [ - 'type' => 'Int', - 'description' => __( 'Product variation stock quantity', 'wp-graphql-woocommerce' ), - ], - 'stockStatus' => [ - 'type' => 'StockStatusEnum', - 'description' => __( 'Product stock status', 'wp-graphql-woocommerce' ), - ], - 'backorders' => [ - 'type' => 'BackordersEnum', - 'description' => __( 'Product variation backorders', 'wp-graphql-woocommerce' ), - ], - 'backordersAllowed' => [ - 'type' => 'Boolean', - 'description' => __( 'Can product be backordered?', 'wp-graphql-woocommerce' ), - ], - 'weight' => [ - 'type' => 'String', - 'description' => __( 'Product variation weight', 'wp-graphql-woocommerce' ), - ], - 'length' => [ - 'type' => 'String', - 'description' => __( 'Product variation length', 'wp-graphql-woocommerce' ), - ], - 'width' => [ - 'type' => 'String', - 'description' => __( 'Product variation width', 'wp-graphql-woocommerce' ), - ], - 'height' => [ - 'type' => 'String', - 'description' => __( 'Product variation height', 'wp-graphql-woocommerce' ), - ], - 'menuOrder' => [ - 'type' => 'Int', - 'description' => __( 'Menu order', 'wp-graphql-woocommerce' ), - ], - 'purchaseNote' => [ - 'type' => 'String', - 'description' => __( 'Product variation purchase_note', 'wp-graphql-woocommerce' ), - ], - 'shippingClass' => [ + 'shippingClass' => [ 'type' => 'String', 'description' => __( 'Product variation shipping class', 'wp-graphql-woocommerce' ), ], - 'catalogVisibility' => [ - 'type' => 'CatalogVisibilityEnum', - 'description' => __( 'Product variation catalog visibility', 'wp-graphql-woocommerce' ), - ], - 'hasAttributes' => [ + 'hasAttributes' => [ 'type' => 'Boolean', 'description' => __( 'Does product variation have any visible attributes', 'wp-graphql-woocommerce' ), ], - 'type' => [ - 'type' => 'ProductTypesEnum', - 'description' => __( 'Product type', 'wp-graphql-woocommerce' ), - ], - 'image' => [ - 'type' => 'MediaItem', - 'description' => __( 'Product variation main image', 'wp-graphql-woocommerce' ), - 'resolve' => static function ( $source, array $args, AppContext $context ) { - return ! empty( $source->image_id ) - ? $context->get_loader( 'post' )->load_deferred( $source->image_id ) - : null; - }, - ], - 'metaData' => Meta_Data_Type::get_metadata_field_definition(), ]; } } diff --git a/includes/type/interface/class-product.php b/includes/type/interface/class-product.php index afcf7c13..ad489e2e 100644 --- a/includes/type/interface/class-product.php +++ b/includes/type/interface/class-product.php @@ -25,11 +25,8 @@ class Product { * @return void */ public static function register_interface() { - // Register the fields to the Product Interface // the product interface is defined by the post_type registration. - register_graphql_fields( 'Product', self::get_fields() ); - register_graphql_field( 'RootQuery', 'product', diff --git a/includes/type/object/class-customer-type.php b/includes/type/object/class-customer-type.php index ca7436da..76079de5 100644 --- a/includes/type/object/class-customer-type.php +++ b/includes/type/object/class-customer-type.php @@ -10,6 +10,7 @@ namespace WPGraphQL\WooCommerce\Type\WPObject; +use GraphQL\Deferred; use GraphQL\Error\UserError; use GraphQL\Type\Definition\ResolveInfo; use WPGraphQL\AppContext; @@ -221,7 +222,7 @@ class Customer_Type { 'Customer', [ 'availablePaymentMethods' => [ - 'type' => [ 'list_of' => 'PaymentToken' ], + 'type' => [ 'list_of' => 'PaymentTokenInterface' ], 'description' => __( 'Customer\'s stored payment tokens.', 'wp-graphql-woocommerce' ), 'resolve' => static function ( $source ) { if ( get_current_user_id() === $source->ID ) { @@ -285,56 +286,130 @@ class Customer_Type { * @return void */ public static function register_session_handler_fields() { - /** - * Register the "sessionToken" field to the "Customer" type. - */ - register_graphql_field( - 'Customer', - 'sessionToken', - [ - 'type' => 'String', - 'description' => __( 'A JWT token that can be used in future requests to for WooCommerce session identification', 'wp-graphql-woocommerce' ), - 'resolve' => static function ( $source ) { - if ( \get_current_user_id() === $source->ID || 'guest' === $source->id ) { - /** - * Session handler. - * - * @var \WPGraphQL\WooCommerce\Utils\QL_Session_Handler $session - */ - $session = \WC()->session; + $token_type = woographql_setting( 'set_session_token_type', 'legacy' ); + if ( in_array( $token_type, [ 'legacy', 'both' ], true ) ) { + /** + * Register the "sessionToken" field to the "Customer" type. + */ + register_graphql_field( + 'Customer', + 'sessionToken', + [ + 'type' => 'String', + 'description' => __( 'A JWT token that can be used in future requests to for WooCommerce session identification', 'wp-graphql-woocommerce' ), + 'resolve' => static function ( $source ) { + if ( \get_current_user_id() === $source->ID || 'guest' === $source->id ) { + return new Deferred( + static function () { + /** + * Session handler. + * + * @var \WPGraphQL\WooCommerce\Utils\QL_Session_Handler $session + */ + $session = \WC()->session; - return apply_filters( 'graphql_customer_session_token', $session->build_token() ); - } + return apply_filters( 'graphql_customer_session_token', $session->build_token() ); + } + ); + } - return null; - }, - ] - ); - /** - * Register the "wooSessionToken" field to the "User" type. - */ - register_graphql_field( - 'User', - 'wooSessionToken', - [ - 'type' => 'String', - 'description' => __( 'A JWT token that can be used in future requests to for WooCommerce session identification', 'wp-graphql-woocommerce' ), - 'resolve' => static function ( $source ) { - if ( \get_current_user_id() === $source->userId ) { // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase - /** - * Session handler - * - * @var \WPGraphQL\WooCommerce\Utils\QL_Session_Handler $session - */ - $session = \WC()->session; + return null; + }, + ] + ); - return apply_filters( 'graphql_customer_session_token', $session->build_token() ); - } + /** + * Register the "wooSessionToken" field to the "User" type. + */ + register_graphql_field( + 'User', + 'wooSessionToken', + [ + 'type' => 'String', + 'description' => __( 'A JWT token that can be used in future requests to for WooCommerce session identification', 'wp-graphql-woocommerce' ), + 'resolve' => static function ( $source ) { + if ( \get_current_user_id() === $source->userId ) { // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase + return new Deferred( + static function () { + /** + * Session handler + * + * @var \WPGraphQL\WooCommerce\Utils\QL_Session_Handler $session + */ + $session = \WC()->session; - return null; - }, - ] - ); + return apply_filters( 'graphql_customer_session_token', $session->build_token() ); + } + ); + } + + return null; + }, + ] + ); + } + + if ( in_array( $token_type, [ 'store-api', 'both' ], true ) ) { + /** + * Register the "cartToken" field to the "Customer" type. + */ + register_graphql_field( + 'Customer', + 'cartToken', + [ + 'type' => 'String', + 'description' => __( 'A JWT token that can be used in future requests to for WooCommerce session identification', 'wp-graphql-woocommerce' ), + 'resolve' => static function ( $source ) { + if ( \get_current_user_id() === $source->ID || 'guest' === $source->id ) { + return new Deferred( + static function () { + /** + * Session handler. + * + * @var \WPGraphQL\WooCommerce\Utils\QL_Session_Handler $session + */ + $session = \WC()->session; + + return apply_filters( 'graphql_cart_token', $session->build_cart_token() ); + } + ); + } + + return null; + }, + ] + ); + + /** + * Register the "cartToken" field to the "User" type. + */ + register_graphql_field( + 'User', + 'cartToken', + [ + 'type' => 'String', + 'description' => __( 'A JWT token that can be used in future requests to for WooCommerce session identification', 'wp-graphql-woocommerce' ), + 'resolve' => static function ( $source ) { + if ( \get_current_user_id() === $source->userId ) { // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase + return new Deferred( + static function () { + /** + * Session handler + * + * @var \WPGraphQL\WooCommerce\Utils\QL_Session_Handler $session + */ + $session = \WC()->session; + + return apply_filters( 'graphql_cart_token', $session->build_cart_token() ); + } + ); + } + + return null; + }, + ] + ); + } } /** diff --git a/includes/type/object/class-order-item-type.php b/includes/type/object/class-order-item-type.php index db55989b..c94d3d4e 100644 --- a/includes/type/object/class-order-item-type.php +++ b/includes/type/object/class-order-item-type.php @@ -12,6 +12,7 @@ namespace WPGraphQL\WooCommerce\Type\WPObject; use WPGraphQL\AppContext; use WPGraphQL\Data\Connection\PostObjectConnectionResolver; +use WPGraphQL\WooCommerce\Data\Connection\Product_Connection_Resolver; use WPGraphQL\WooCommerce\Data\Factory; /** @@ -211,7 +212,7 @@ class Order_Item_Type { 'oneToOne' => true, 'resolve' => static function ( $source, array $args, AppContext $context, $info ) { $id = $source->productId; // @phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase - $resolver = new PostObjectConnectionResolver( $source, $args, $context, $info, 'product' ); + $resolver = new Product_Connection_Resolver( $source, $args, $context, $info ); return $resolver ->one_to_one() diff --git a/includes/type/object/class-payment-token-types.php b/includes/type/object/class-payment-token-types.php index cd961a40..c56d0917 100644 --- a/includes/type/object/class-payment-token-types.php +++ b/includes/type/object/class-payment-token-types.php @@ -10,7 +10,7 @@ namespace WPGraphQL\WooCommerce\Type\WPObject; -use WPGraphQL\WooCommerce\Type\WPInterface\Payment_Token; +use WPGraphQL\WooCommerce\Type\WPInterface\Payment_Token_Interface; /** * Class Payment_Token_Types @@ -22,21 +22,29 @@ class Payment_Token_Types { * @return void */ public static function register() { + register_graphql_object_type( + 'PaymentToken', + [ + 'description' => __( 'A payment token', 'wp-graphql-woocommerce' ), + 'interfaces' => [ 'PaymentTokenInterface' ], + 'fields' => [], + ] + ); register_graphql_object_type( 'PaymentTokenCC', [ - 'description' => __( 'A credit cart payment token', 'wp-graphql-woocommerce' ), - 'interfaces' => [ 'PaymentToken' ], - 'fields' => Payment_Token::get_fields( self::get_credit_card_fields() ), + 'description' => __( 'A credit card payment token', 'wp-graphql-woocommerce' ), + 'interfaces' => [ 'PaymentTokenInterface' ], + 'fields' => Payment_Token_Interface::get_fields( self::get_credit_card_fields() ), ] ); register_graphql_object_type( 'PaymentTokenECheck', [ - 'description' => __( 'A electronic check payment token', 'wp-graphql-woocommerce' ), - 'interfaces' => [ 'PaymentToken' ], - 'fields' => Payment_Token::get_fields( self::get_e_check_fields() ), + 'description' => __( 'An electronic check payment token', 'wp-graphql-woocommerce' ), + 'interfaces' => [ 'PaymentTokenInterface' ], + 'fields' => Payment_Token_Interface::get_fields( self::get_e_check_fields() ), ] ); } diff --git a/includes/type/object/class-shipping-rate-type.php b/includes/type/object/class-shipping-rate-type.php index e9e00edb..6afc7ab1 100644 --- a/includes/type/object/class-shipping-rate-type.php +++ b/includes/type/object/class-shipping-rate-type.php @@ -54,10 +54,10 @@ class Shipping_Rate_Type { }, ], 'cost' => [ - 'type' => 'String', + 'type' => 'Float', 'description' => __( 'Shipping rate cost', 'wp-graphql-woocommerce' ), 'resolve' => static function ( $source ) { - return ! empty( $source->get_cost() ) ? $source->get_cost() : null; + return ! is_null( $source->get_cost() ) ? $source->get_cost() : null; }, ], ], diff --git a/includes/utils/class-ql-session-handler.php b/includes/utils/class-ql-session-handler.php index 2da992c1..5cd0c1d4 100644 --- a/includes/utils/class-ql-session-handler.php +++ b/includes/utils/class-ql-session-handler.php @@ -118,6 +118,8 @@ class QL_Session_Handler extends WC_Session_Handler { add_action( 'woocommerce_set_cart_cookies', [ $this, 'set_customer_session_token' ], 10 ); add_action( 'woographql_update_session', [ $this, 'set_customer_session_token' ], 10 ); add_action( 'shutdown', [ $this, 'save_data' ] ); + add_filter( 'graphql_jwt_auth_after_authenticate', [ $this, 'reinitialize_session_token' ], 10 ); + add_filter( 'graphql_login_payload', [ $this, 'reinitialize_session_token' ], 10 ); } else { add_action( 'woocommerce_set_cart_cookies', [ $this, 'set_customer_session_cookie' ], 10 ); add_action( 'shutdown', [ $this, 'save_data' ], 20 ); @@ -185,11 +187,11 @@ class QL_Session_Handler extends WC_Session_Handler { if ( $token->exp < $this->_session_expiration ) { $this->update_session_timestamp( (string) $this->_customer_id, $this->_session_expiration ); } - } elseif ( is_wp_error( $token ) ) { + } elseif ( Router::is_graphql_http_request() && is_wp_error( $token ) ) { add_filter( 'graphql_woocommerce_session_token_errors', static function ( $errors ) use ( $token ) { - $errors = $token->get_error_message(); + $errors = $token->get_error_code() . ': ' . $token->get_error_message(); return $errors; } ); @@ -214,6 +216,29 @@ class QL_Session_Handler extends WC_Session_Handler { } } + /** + * Reinitialize session token in response after authentication in GraphQL. + * + * @param array $response The authentication response. + * + * @return array + */ + public function reinitialize_session_token( $response ) { + $this->init_session_token(); + + $token = $this->build_token(); + if ( $token ) { + $response['session_token'] = $token; + } + + // Add Store API Cart-Token if enabled. + $cart_token = $this->build_cart_token(); + if ( ! empty( $cart_token ) ) { + $response['cart_token'] = $cart_token; + } + return $response; + } + /** * Retrieve and decrypt the session data from session, if set. Otherwise return false. * @@ -480,7 +505,7 @@ class QL_Session_Handler extends WC_Session_Handler { * * @return string|null Cart-Token JWT or null if feature disabled or unavailable. */ - protected function build_cart_token() { + public function build_cart_token() { // Check if Store API token generation is enabled. $token_type = woographql_setting( 'set_session_token_type', 'legacy' ); if ( ! in_array( $token_type, [ 'store-api', 'both' ], true ) ) { @@ -633,10 +658,9 @@ class QL_Session_Handler extends WC_Session_Handler { */ public function set_session_expiration() { $this->_session_issued = time(); - // 47 hours. - $this->_session_expiring = apply_filters( 'wc_session_expiring', $this->_session_issued + ( 60 * 60 * 47 ) ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound - // 48 hours. - $this->_session_expiration = apply_filters( 'wc_session_expiration', $this->_session_issued + ( 60 * 60 * 48 ) ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound + + parent::set_session_expiration(); + $this->_session_expiration = apply_filters_deprecated( 'graphql_woocommerce_cart_session_expire', [ $this->_session_expiration ], diff --git a/tests/_support/Helper/GraphQLE2E.php b/tests/_support/Helper/GraphQLE2E.php index 2bf1ffde..7c532fa2 100644 --- a/tests/_support/Helper/GraphQLE2E.php +++ b/tests/_support/Helper/GraphQLE2E.php @@ -939,6 +939,14 @@ class GraphQLE2E extends \Codeception\Module { $zone->add_shipping_method( 'flat_rate' ); $zone->add_shipping_method( 'free_shipping' ); + $zone = new \WC_Shipping_Zone(); + $zone->set_zone_name( 'US' ); + $zone->set_zone_order( 5 ); + $zone->add_location( 'US:NY', 'state' ); + $zone->save(); + $zone->add_shipping_method( 'flat_rate' ); + $zone->add_shipping_method( 'free_shipping' ); + global $wp_rewrite; //Write the rule diff --git a/tests/acceptance/ReturningCustomerSessionCept.php b/tests/acceptance/ReturningCustomerSessionCept.php index db168c84..7ccb74f1 100644 --- a/tests/acceptance/ReturningCustomerSessionCept.php +++ b/tests/acceptance/ReturningCustomerSessionCept.php @@ -319,7 +319,7 @@ $authToken = $success['data']['login']['authToken']; // any existing sessions. $new_session_token = $success['data']['login']['sessionToken']; -$I->wantTo( 'Check the cart and should be empty' ); +$I->wantTo( 'Check the cart and should contain the contents from our guest session' ); $cart_query = ' query { @@ -346,14 +346,5 @@ $response = $I->sendGraphQLRequest( 'woocommerce-session' => "Session {$new_session_token}", ] ); -$expected_results = [ - 'data' => [ - 'cart' => [ - 'contents' => [ - 'nodes' => [], - ], - ], - ], -]; $I->assertEquals( $expected_results, $response ); diff --git a/tests/functional/QLSessionHandlerCest.php b/tests/functional/QLSessionHandlerCest.php index a88fcb5f..5314d406 100644 --- a/tests/functional/QLSessionHandlerCest.php +++ b/tests/functional/QLSessionHandlerCest.php @@ -488,6 +488,7 @@ class QLSessionHandlerCest { $actual, [ $I->expectField( 'cart.contents.nodes.#.key', $cart_item_key ), + $I->expectField( 'cart.availableShippingMethods', Signal::NOT_FALSY ), $I->expectNode( 'cart.availableShippingMethods', [ @@ -496,14 +497,14 @@ class QLSessionHandlerCest { $I->expectNode( 'rates', [ - $I->expectField( 'cost', '0.00' ), + $I->expectField( 'cost', 0 ), $I->expectField( 'label', 'Flat rate' ), ] ), $I->expectNode( 'rates', [ - $I->expectField( 'cost', '0.00' ), + $I->expectField( 'cost', 0 ), $I->expectField( 'label', 'Free shipping' ), ] ), diff --git a/tests/wpunit/CustomerQueriesTest.php b/tests/wpunit/CustomerQueriesTest.php index 4e7fa34a..d560cb06 100644 --- a/tests/wpunit/CustomerQueriesTest.php +++ b/tests/wpunit/CustomerQueriesTest.php @@ -496,8 +496,6 @@ class CustomerQueriesTest extends \Tests\WPGraphQL\WooCommerce\TestCase\WooGraph customer(id: $id) { id availablePaymentMethods { - id - tokenId ... on PaymentTokenCC { last4 expiryMonth @@ -509,16 +507,12 @@ class CustomerQueriesTest extends \Tests\WPGraphQL\WooCommerce\TestCase\WooGraph } } availablePaymentMethodsCC { - id - tokenId last4 expiryMonth expiryYear cardType } availablePaymentMethodsEC { - id - tokenId last4 } } @@ -554,8 +548,6 @@ class CustomerQueriesTest extends \Tests\WPGraphQL\WooCommerce\TestCase\WooGraph $this->expectedNode( 'customer.availablePaymentMethods', [ - $this->expectedField( 'id', $this->toRelayId( 'token', $token_cc->get_id() ) ), - $this->expectedField( 'tokenId', $token_cc->get_id() ), $this->expectedField( 'last4', 1234 ), $this->expectedField( 'expiryMonth', $expiry_month ), $this->expectedField( 'expiryYear', $expiry_year ), @@ -565,8 +557,6 @@ class CustomerQueriesTest extends \Tests\WPGraphQL\WooCommerce\TestCase\WooGraph $this->expectedNode( 'customer.availablePaymentMethodsCC', [ - $this->expectedField( 'id', $this->toRelayId( 'token', $token_cc->get_id() ) ), - $this->expectedField( 'tokenId', $token_cc->get_id() ), $this->expectedField( 'last4', 1234 ), $this->expectedField( 'expiryMonth', $expiry_month ), $this->expectedField( 'expiryYear', $expiry_year ), @@ -576,8 +566,6 @@ class CustomerQueriesTest extends \Tests\WPGraphQL\WooCommerce\TestCase\WooGraph $this->expectedNode( 'customer.availablePaymentMethodsEC', [ - $this->not()->expectedField( 'id', $this->toRelayId( 'token', $token_cc->get_id() ) ), - $this->not()->expectedField( 'tokenId', $token_cc->get_id() ), $this->not()->expectedField( 'last4', 1234 ), $this->not()->expectedField( 'expiryMonth', $expiry_month ), $this->not()->expectedField( 'expiryYear', $expiry_year ), @@ -587,24 +575,18 @@ class CustomerQueriesTest extends \Tests\WPGraphQL\WooCommerce\TestCase\WooGraph $this->expectedNode( 'customer.availablePaymentMethods', [ - $this->expectedField( 'id', $this->toRelayId( 'token', $token_ec->get_id() ) ), - $this->expectedField( 'tokenId', $token_ec->get_id() ), $this->expectedField( 'last4', 4567 ), ] ), $this->expectedNode( 'customer.availablePaymentMethodsCC', [ - $this->not()->expectedField( 'id', $this->toRelayId( 'token', $token_ec->get_id() ) ), - $this->not()->expectedField( 'tokenId', $token_ec->get_id() ), $this->not()->expectedField( 'last4', 4567 ), ] ), $this->expectedNode( 'customer.availablePaymentMethodsEC', [ - $this->expectedField( 'id', $this->toRelayId( 'token', $token_ec->get_id() ) ), - $this->expectedField( 'tokenId', $token_ec->get_id() ), $this->expectedField( 'last4', 4567 ), ] ), diff --git a/tests/wpunit/PaymentMethodMutationsTest.php b/tests/wpunit/PaymentMethodMutationsTest.php index 8c591de8..79097d79 100644 --- a/tests/wpunit/PaymentMethodMutationsTest.php +++ b/tests/wpunit/PaymentMethodMutationsTest.php @@ -33,8 +33,14 @@ class PaymentMethodMutationsTest extends \Tests\WPGraphQL\WooCommerce\TestCase\W setDefaultPaymentMethod(input: { tokenId: $tokenId }) { status token { - id + type isDefault + ... on PaymentTokenCC { + last4 + } + ... on PaymentTokenECheck { + last4 + } } } } @@ -67,7 +73,7 @@ class PaymentMethodMutationsTest extends \Tests\WPGraphQL\WooCommerce\TestCase\W $this->expectedObject( 'setDefaultPaymentMethod.token', [ - $this->expectedField( 'id', $this->toRelayId( 'token', $token_ec->get_id() ) ), + $this->expectedField( 'type', 'eCheck' ), $this->expectedField( 'isDefault', true ), ] ), @@ -83,7 +89,7 @@ class PaymentMethodMutationsTest extends \Tests\WPGraphQL\WooCommerce\TestCase\W $this->expectedObject( 'setDefaultPaymentMethod.token', [ - $this->expectedField( 'id', $this->toRelayId( 'token', $token_cc->get_id() ) ), + $this->expectedField( 'type', 'CC' ), $this->expectedField( 'isDefault', true ), ] ), diff --git a/wp-graphql-woocommerce.php b/wp-graphql-woocommerce.php index 8dc128ef..29e8cc80 100644 --- a/wp-graphql-woocommerce.php +++ b/wp-graphql-woocommerce.php @@ -163,6 +163,28 @@ function init_auth_router() { } add_action( 'plugins_loaded', 'WPGraphQL\WooCommerce\init_auth_router' ); +/** + * Prevent WooCommerce from loading cart during 'init' for GraphQL requests. + * + * WooCommerce calls wc_load_cart() during the 'init' action for frontend requests. + * This happens before JWT authentication can set the current user, causing the + * session to be initialized with the wrong user context. + * + * By making WooCommerce think GraphQL requests are REST API requests, we prevent + * the early cart loading. The cart is then loaded later during 'graphql_before_execute' + * after JWT authentication has had a chance to run. + * + * @return void + */ +function prevent_early_wc_cart_loading() { + if ( ! is_graphql_http_request() ) { + return; + } + + add_filter( 'woocommerce_is_rest_api_request', '__return_true' ); +} +add_action( 'plugins_loaded', 'WPGraphQL\WooCommerce\prevent_early_wc_cart_loading', 0 ); + // Load constants. constants();