diff --git a/client/express-checkout/event-handlers.js b/client/express-checkout/event-handlers.js index 9a146e1ecb4..d293eaf72e7 100644 --- a/client/express-checkout/event-handlers.js +++ b/client/express-checkout/event-handlers.js @@ -51,7 +51,29 @@ export const shippingAddressChangeHandler = async ( event, elements ) => { // when no shipping options are returned, the API still returns a 200 status code. // We need to ensure that shipping options are present - otherwise the ECE dialog won't update correctly. if ( shippingRates.length === 0 ) { - event.reject(); + // Check if this is because no shipping zones are configured. + const hasShippingZones = + getExpressCheckoutData( 'checkout' )?.has_shipping_zones ?? + true; + + if ( ! hasShippingZones ) { + // Show error message about missing shipping configuration + event.reject( { + code: 'shipping_zones_not_configured', + message: __( + 'No shipping methods are available. Please contact the store administrator to configure shipping.', + 'woocommerce-payments' + ), + } ); + } else { + event.reject( { + code: 'no_shipping_options', + message: __( + 'No shipping options are available for this address.', + 'woocommerce-payments' + ), + } ); + } return; } @@ -69,7 +91,13 @@ export const shippingAddressChangeHandler = async ( event, elements ) => { lineItems: transformCartDataForDisplayItems( cartData ), } ); } catch ( error ) { - event.reject(); + event.reject( { + code: 'shipping_error', + message: __( + 'There was an error processing the shipping address.', + 'woocommerce-payments' + ), + } ); } }; diff --git a/includes/express-checkout/class-wc-payments-express-checkout-button-handler.php b/includes/express-checkout/class-wc-payments-express-checkout-button-handler.php index 247c31b6005..516c600ec43 100644 --- a/includes/express-checkout/class-wc-payments-express-checkout-button-handler.php +++ b/includes/express-checkout/class-wc-payments-express-checkout-button-handler.php @@ -245,6 +245,7 @@ public function get_express_checkout_params() { 'needs_payer_phone' => 'required' === get_option( 'woocommerce_checkout_phone_field', 'required' ), 'allowed_shipping_countries' => array_keys( WC()->countries->get_shipping_countries() ?? [] ), 'display_prices_with_tax' => 'incl' === get_option( 'woocommerce_tax_display_cart' ), + 'has_shipping_zones' => $this->express_checkout_helper->has_shipping_zones_configured(), ], 'button' => $this->get_button_settings(), 'login_confirmation' => $this->get_login_confirmation_settings(), diff --git a/includes/express-checkout/class-wc-payments-express-checkout-button-helper.php b/includes/express-checkout/class-wc-payments-express-checkout-button-helper.php index 2cb442d08b2..5819abffb3b 100644 --- a/includes/express-checkout/class-wc-payments-express-checkout-button-helper.php +++ b/includes/express-checkout/class-wc-payments-express-checkout-button-helper.php @@ -445,6 +445,29 @@ public function should_show_express_checkout_button() { return false; } + // Check if shipping is required but no shipping zones are configured. + $needs_shipping = false; + + if ( $this->is_product() ) { + $needs_shipping = $this->get_product()->needs_shipping(); + } elseif ( $this->is_cart() || $this->is_checkout() ) { + // Check if any products in cart need shipping. + $needs_shipping = false; + foreach ( WC()->cart->get_cart() as $cart_item ) { + if ( $cart_item['data']->needs_shipping() ) { + $needs_shipping = true; + break; + } + } + } + + if ( $needs_shipping ) { + if ( ! $this->has_shipping_zones_configured() ) { + Logger::log( 'Shipping required but no shipping zones configured ( Express Checkout Element button disabled )' ); + return false; + } + } + return true; } @@ -463,6 +486,39 @@ public function product_needs_shipping( WC_Product $product ) { return wc_shipping_enabled() && 0 !== wc_get_shipping_method_count( true ) && $product->needs_shipping(); } + /** + * Check if shipping zones are properly configured. + * + * @return bool Returns true if shipping zones are configured; otherwise, returns false. + */ + public function has_shipping_zones_configured() { + if ( ! wc_shipping_enabled() ) { + return false; + } + + $shipping_zones = WC_Shipping_Zones::get_zones(); + $rest_of_world_zone = WC_Shipping_Zones::get_zone_by( 'zone_id', 0 ); + + // Check if there are any shipping zones configured. + if ( empty( $shipping_zones ) && ( ! $rest_of_world_zone || empty( $rest_of_world_zone->get_shipping_methods() ) ) ) { + return false; + } + + // Check if any zone has shipping methods. + foreach ( $shipping_zones as $zone ) { + if ( ! empty( $zone->get_shipping_methods() ) ) { + return true; + } + } + + // Check rest of world zone. + if ( $rest_of_world_zone && ! empty( $rest_of_world_zone->get_shipping_methods() ) ) { + return true; + } + + return false; + } + /** * Checks to make sure product type is supported. * @@ -632,78 +688,6 @@ public function get_product_data() { return apply_filters( 'wcpay_payment_request_product_data', $data, $product ); } - /** - * The Store API doesn't allow checkout without the billing email address present on the order data. - * https://github.com/woocommerce/woocommerce/issues/48540 - * - * @return bool - */ - private function is_pay_for_order_supported() { - $order_id = absint( get_query_var( 'order-pay' ) ); - if ( 0 === $order_id ) { - return false; - } - - $order = wc_get_order( $order_id ); - if ( ! is_a( $order, 'WC_Order' ) ) { - return false; - } - - // we don't need to check its validity or value, we just need to ensure a billing email is present. - $billing_email = $order->get_billing_email(); - if ( ! empty( $billing_email ) ) { - return true; - } - - Logger::log( 'Billing email not present ( Express Checkout Element button disabled )' ); - - return false; - } - - /** - * Whether product page has a supported product. - * - * @return boolean - */ - private function is_product_supported() { - $product = $this->get_product(); - $is_supported = true; - - /** - * Ignore undefined classes from 3rd party plugins. - * - * @psalm-suppress UndefinedClass - */ - - if ( is_null( $product ) || ! is_object( $product ) ) { - $is_supported = false; - } else { - // Simple subscription that needs shipping with free trials is not supported. - $is_free_trial_simple_subs = class_exists( 'WC_Subscriptions_Product' ) && $product->get_type() === 'subscription' && $product->needs_shipping() && WC_Subscriptions_Product::get_trial_length( $product ) > 0; - - if ( - ! in_array( $product->get_type(), $this->supported_product_types(), true ) - || $is_free_trial_simple_subs - || ( class_exists( 'WC_Pre_Orders_Product' ) && WC_Pre_Orders_Product::product_is_charged_upon_release( $product ) ) // Pre Orders charge upon release not supported. - || ( class_exists( 'WC_Composite_Products' ) && $product->is_type( 'composite' ) ) // Composite products are not supported on the product page. - || ( class_exists( 'WC_Mix_and_Match' ) && $product->is_type( 'mix-and-match' ) ) // Mix and match products are not supported on the product page. - ) { - $is_supported = false; - } elseif ( class_exists( 'WC_Product_Addons_Helper' ) ) { - // File upload addon not supported. - $product_addons = WC_Product_Addons_Helper::get_product_addons( $product->get_id() ); - foreach ( $product_addons as $addon ) { - if ( 'file_upload' === $addon['type'] ) { - $is_supported = false; - break; - } - } - } - } - - return apply_filters( 'wcpay_payment_request_is_product_supported', $is_supported, $product ); - } - /** * Gets the product total price. * @@ -836,4 +820,76 @@ public function add_order_payment_method_title( $order_id ) { $order->set_payment_method_title( $payment_method_title . $suffix ); $order->save(); } + + /** + * The Store API doesn't allow checkout without the billing email address present on the order data. + * https://github.com/woocommerce/woocommerce/issues/48540 + * + * @return bool + */ + private function is_pay_for_order_supported() { + $order_id = absint( get_query_var( 'order-pay' ) ); + if ( 0 === $order_id ) { + return false; + } + + $order = wc_get_order( $order_id ); + if ( ! is_a( $order, 'WC_Order' ) ) { + return false; + } + + // we don't need to check its validity or value, we just need to ensure a billing email is present. + $billing_email = $order->get_billing_email(); + if ( ! empty( $billing_email ) ) { + return true; + } + + Logger::log( 'Billing email not present ( Express Checkout Element button disabled )' ); + + return false; + } + + /** + * Whether product page has a supported product. + * + * @return boolean + */ + private function is_product_supported() { + $product = $this->get_product(); + $is_supported = true; + + /** + * Ignore undefined classes from 3rd party plugins. + * + * @psalm-suppress UndefinedClass + */ + + if ( is_null( $product ) || ! is_object( $product ) ) { + $is_supported = false; + } else { + // Simple subscription that needs shipping with free trials is not supported. + $is_free_trial_simple_subs = class_exists( 'WC_Subscriptions_Product' ) && $product->get_type() === 'subscription' && $product->needs_shipping() && WC_Subscriptions_Product::get_trial_length( $product ) > 0; + + if ( + ! in_array( $product->get_type(), $this->supported_product_types(), true ) + || $is_free_trial_simple_subs + || ( class_exists( 'WC_Pre_Orders_Product' ) && WC_Pre_Orders_Product::product_is_charged_upon_release( $product ) ) // Pre Orders charge upon release not supported. + || ( class_exists( 'WC_Composite_Products' ) && $product->is_type( 'composite' ) ) // Composite products are not supported on the product page. + || ( class_exists( 'WC_Mix_and_Match' ) && $product->is_type( 'mix-and-match' ) ) // Mix and match products are not supported on the product page. + ) { + $is_supported = false; + } elseif ( class_exists( 'WC_Product_Addons_Helper' ) ) { + // File upload addon not supported. + $product_addons = WC_Product_Addons_Helper::get_product_addons( $product->get_id() ); + foreach ( $product_addons as $addon ) { + if ( 'file_upload' === $addon['type'] ) { + $is_supported = false; + break; + } + } + } + } + + return apply_filters( 'wcpay_payment_request_is_product_supported', $is_supported, $product ); + } } diff --git a/tests/unit/express-checkout/test-class-wc-payments-express-checkout-button-helper.php b/tests/unit/express-checkout/test-class-wc-payments-express-checkout-button-helper.php index 8bced3faef3..79439e7ebe4 100644 --- a/tests/unit/express-checkout/test-class-wc-payments-express-checkout-button-helper.php +++ b/tests/unit/express-checkout/test-class-wc-payments-express-checkout-button-helper.php @@ -119,6 +119,46 @@ public function __return_base() { return 'base'; } + /** + * Test has_shipping_zones_configured method with no shipping zones. + */ + public function test_has_shipping_zones_configured_no_zones() { + // Delete all shipping zones. + WC_Helper_Shipping::delete_simple_flat_rate(); + $this->zone->delete(); + + // Create a new zone with no methods. + $empty_zone = new WC_Shipping_Zone(); + $empty_zone->set_zone_name( 'Empty Zone' ); + $empty_zone->save(); + + $this->assertFalse( $this->system_under_test->has_shipping_zones_configured() ); + + // Clean up. + $empty_zone->delete(); + } + + /** + * Test has_shipping_zones_configured method with configured shipping zones. + */ + public function test_has_shipping_zones_configured_with_zones() { + // The zone is already set up in set_up() with shipping methods. + $this->assertTrue( $this->system_under_test->has_shipping_zones_configured() ); + } + + /** + * Test has_shipping_zones_configured method when shipping is disabled. + */ + public function test_has_shipping_zones_configured_shipping_disabled() { + // Disable shipping. + update_option( 'woocommerce_ship_to_countries', 'disabled' ); + + $this->assertFalse( $this->system_under_test->has_shipping_zones_configured() ); + + // Re-enable shipping. + update_option( 'woocommerce_ship_to_countries', 'all' ); + } + /** * @return WC_Payment_Gateway_WCPay */