diff --git a/changelog.txt b/changelog.txt index 030353c64..627bded6e 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,6 +1,7 @@ *** WooCommerce Shipping & Tax Changelog *** -= 2.8.8 - 2025-xx-xx = += 2.9.0 - 2025-xx-xx = +* Add - Display a notice if the user does not use a 9-digit zip code. * Tweak - WooCommerce 9.7 Compatibility. = 2.8.7 - 2025-01-20 = diff --git a/classes/class-wc-connect-blocks-integration.php b/classes/class-wc-connect-blocks-integration.php new file mode 100644 index 000000000..eed376005 --- /dev/null +++ b/classes/class-wc-connect-blocks-integration.php @@ -0,0 +1,118 @@ +register_scripts(); + } + + /** + * Returns an array of script handles to enqueue in the frontend context. + * + * @return string[] + */ + public function get_script_handles(): array { + return array( 'woocommerce-services-checkout-' . WC_Connect_Loader::get_wcs_version() ); + } + + /** + * Returns an array of script handles to enqueue in the editor context. + * + * @return string[] + */ + public function get_editor_script_handles(): array { + return array(); + } + + /** + * An array of key, value pairs of data made available to the block on the client side. + * + * @return array + */ + public function get_script_data(): array { + return array(); + } + + /** + * Registers the scripts and styles for the integration. + */ + public function register_scripts() { + + foreach ( $this->get_script_handles() as $handle ) { + $this->register_script( $handle ); + } + } + + /** + * Register a script for the integration. + * + * @param string $handle Script handle. + */ + protected function register_script( string $handle ) { + $script_path = $handle . '.js'; + $script_url = WC_Connect_Loader::get_wc_connect_base_url() . $script_path; + + $script_asset_path = WC_Connect_Loader::get_wc_connect_base_path() . $handle . '.asset.php'; + $script_asset = file_exists( $script_asset_path ) + ? require $script_asset_path // nosemgrep: audit.php.lang.security.file.inclusion-arg --- This is a safe file inclusion. + : array( + 'dependencies' => array(), + 'version' => $this->get_file_version( WC_Connect_Loader::get_wc_connect_base_path() . $script_path ), + ); + + wp_register_script( + $handle, + $script_url, + $script_asset['dependencies'], + $script_asset['version'], + true + ); + + wp_set_script_translations( + $handle, + 'woocommerce-services', + WC_Connect_Loader::get_wcs_abs_path() . 'i18n/languages' + ); + } + + /** + * Get the file modified time as a cache buster if we're in dev mode. + * + * @param string $file Local path to the file. + * + * @return string The cache buster value to use for the given file. + */ + protected function get_file_version( string $file ): string { + if ( defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG && file_exists( $file ) ) { + return filemtime( $file ); + } + + return WC_Connect_Loader::get_wcs_version(); + } +} diff --git a/classes/class-wc-connect-store-api-extension.php b/classes/class-wc-connect-store-api-extension.php new file mode 100644 index 000000000..5d48ff20b --- /dev/null +++ b/classes/class-wc-connect-store-api-extension.php @@ -0,0 +1,91 @@ +get( ExtendSchema::class ); + self::extend_store(); + } + + /** + * Registers the data into each endpoint. + */ + public static function extend_store() { + + self::$extend->register_endpoint_data( + array( + 'endpoint' => CartSchema::IDENTIFIER, + 'namespace' => self::IDENTIFIER, + 'data_callback' => array( static::class, 'data' ), + 'schema_callback' => array( static::class, 'schema' ), + 'schema_type' => ARRAY_A, + ) + ); + } + + /** + * Store API extension data callback. + * + * @return array + */ + public static function data() { + $notices = WC()->session->get( WC_Connect_TaxJar_Integration::NOTICE_KEY ); + $notices = is_array( $notices ) ? $notices : array(); + + return array( + 'error_notices' => $notices, + ); + } + + /** + * Store API extension schema callback. + * + * @return array Registered schema. + */ + public static function schema() { + return array( + 'error_notices' => array( + 'description' => __( 'Error notices from TaxJar operation.', 'woocommerce-services' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + ); + } +} diff --git a/classes/class-wc-connect-taxjar-integration.php b/classes/class-wc-connect-taxjar-integration.php index 7f8e2864b..517ad4837 100644 --- a/classes/class-wc-connect-taxjar-integration.php +++ b/classes/class-wc-connect-taxjar-integration.php @@ -58,6 +58,7 @@ class WC_Connect_TaxJar_Integration { const PROXY_PATH = 'taxjar/v2'; const OPTION_NAME = 'wc_connect_taxes_enabled'; const SETUP_WIZARD_OPTION_NAME = 'woocommerce_setup_automated_taxes'; + const NOTICE_KEY = 'woocommerce_services_notices'; public function __construct( WC_Connect_API_Client $api_client, @@ -400,10 +401,31 @@ public function _log( $message ) { } /** - * @param $message + * Display notice in cart or checkout page. + * + * @param string|array $message Error message. + */ + public function _notice( $message ) { + $formatted_message = is_scalar( $message ) ? $message : wp_json_encode( $message ); + + // if on checkout page load (not ajax), don't set an error as it prevents checkout page from displaying + if ( + ( is_cart() || ( is_checkout() && is_ajax() ) ) || + ( WC_Connect_Functions::has_cart_or_checkout_block() || WC_Connect_functions::is_store_api_call() ) + ) { + $this->maybe_add_error_notice( $message, 'notice' ); + } + + return; + } + + /** + * Display error on cart or checkout page. + * + * @param string|array $message Error message. */ public function _error( $message ) { - $formatted_message = is_scalar( $message ) ? $message : json_encode( $message ); + $formatted_message = is_scalar( $message ) ? $message : wp_json_encode( $message ); // ignore error messages caused by customer input $state_zip_mismatch = false !== strpos( $formatted_message, 'to_zip' ) && false !== strpos( $formatted_message, 'is not used within to_state' ); @@ -425,13 +447,11 @@ public function _error( $message ) { } // if on checkout page load (not ajax), don't set an error as it prevents checkout page from displaying - if ( ( - ( is_cart() || ( is_checkout() && is_ajax() ) ) || - ( WC_Connect_Functions::has_cart_or_checkout_block() || WC_Connect_functions::is_store_api_call() ) - ) - && ! wc_has_notice( $message, 'error' ) + if ( + ( is_cart() || ( is_checkout() && is_ajax() ) ) || + ( WC_Connect_Functions::has_cart_or_checkout_block() || WC_Connect_functions::is_store_api_call() ) ) { - wc_add_notice( $message, 'error' ); + $this->maybe_add_error_notice( $message, 'error' ); } return; @@ -1061,6 +1081,9 @@ public function maybe_apply_taxjar_nexus_addresses_workaround( $body ) { */ public function calculate_tax( $options = array() ) { $this->_log( ':::: TaxJar Plugin requested ::::' ); + + // Unset the error notice. + WC()->session->set( self::NOTICE_KEY, array() ); // Process $options array and turn them into variables $options = is_array( $options ) ? $options : array(); @@ -1135,6 +1158,8 @@ public function calculate_tax( $options = array() ) { $body['line_items'] = $line_items; } + $this->maybe_add_taxjar_suggestion( $body ); + $response = $this->smartcalcs_cache_request( wp_json_encode( $body ) ); // if no response, no need to keep going - bail early @@ -1301,6 +1326,22 @@ public function create_or_update_tax_rate( $taxjar_response, $location, $rate, $ return $rate_id; } + /** + * Add suggestion for a better TaxJar result. + * + * @param array $body_request Body TaxJar request. + */ + public function maybe_add_taxjar_suggestion( $body_request ) { + if ( + ! empty( $body_request['to_country'] ) && + ! empty( $body_request['to_zip'] ) && + 'US' === $body_request['to_country'] && + ! ( (bool) preg_match( '/^([0-9]{5})([-]?)([0-9]{4})$/', $body_request['to_zip'] ) ) + ) { + $this->_notice( sprintf( __( 'Use 9 digits zip code for more accurate tax calculation. %1$sClick here for zip code look up%2$s.', 'woocommerce-services' ), '', '' ) ); + } + } + /** * Validate TaxJar API request json value and add the error to log. * @@ -1429,6 +1470,26 @@ public function smartcalcs_request( $json ) { } } + public function maybe_add_error_notice( $message, $type = 'error' ) { + if ( ! wc_has_notice( $message, $type ) ) { + wc_add_notice( $message, $type ); + } + + $this->add_error_notice( $message, $type ); + } + + public function add_error_notice( $message, $type = 'error' ) { + $notices = WC()->session->get( self::NOTICE_KEY ); + + if ( ! is_array( $notices ) ) { + $notices = array(); + } + + $notices[ $type ] = $message; + + WC()->session->set( self::NOTICE_KEY, $notices ); + } + /** * Exports existing tax rates to a CSV and clears the table. * diff --git a/client/checkout-notices/index.js b/client/checkout-notices/index.js new file mode 100644 index 000000000..25d2c3632 --- /dev/null +++ b/client/checkout-notices/index.js @@ -0,0 +1,74 @@ +/** + * Block Notices + * + * This file is responsible for rendering Abort Messages from the WooCommerce Shipping & Tax plugin. + * + * @package WooCommerce_Services + */ + +const { useSelect } = window.wp.data; +const { registerPlugin } = window.wp.plugins; +const { ExperimentalOrderShippingPackages, StoreNotice } = window.wc.blocksCheckout; +const { RawHTML } = window.wp.element; + +const createStoreNotice = ( notice, index, type = 'info' ) => { + if ( 'debug' === type ) { + type = 'info'; + } + + // eslint-disable-next-line react/react-in-jsx-scope + const message = {notice}; + + return ( + // eslint-disable-next-line react/react-in-jsx-scope + + {message} + + ); +}; + +const Notices = ({ messages }) => { + if ( ! messages.notice ) { + return null; + } + + const currentMessage = messages.notice; + + return ( + // eslint-disable-next-line react/react-in-jsx-scope +
+ {createStoreNotice( currentMessage, 0, 'info' )} +
+ ); +}; + +const render = () => { + const { errorNotices } = useSelect((select) => { + const storeCartData = select( 'wc/store/cart' ).getCartData(); + + if ( ! storeCartData.extensions || ! storeCartData.extensions.woocommerce_services || ! storeCartData.extensions.woocommerce_services.error_notices ) { + return {}; + } + // eslint-disable-next-line no-shadow + const errorNotices = storeCartData.extensions.woocommerce_services.error_notices; + + return { + errorNotices, + }; + }, []); + + // Ensure we only show abort messages if no shipping rates are available. + if ( ! errorNotices ) { + return null; + } + + return ( + // eslint-disable-next-line react/react-in-jsx-scope + + ); +}; + +registerPlugin('woocommerce-services-notices', { + render, + scope: 'woocommerce-checkout', +}); diff --git a/readme.txt b/readme.txt index 67672f06c..3677b4a9c 100644 --- a/readme.txt +++ b/readme.txt @@ -81,7 +81,8 @@ The source code is freely available [in GitHub](https://github.com/Automattic/wo == Changelog == -= 2.8.8 - 2025-xx-xx = += 2.9.0 - 2025-xx-xx = +* Add - Display a notice if the user does not use a 9-digit zip code. * Tweak - WooCommerce 9.7 Compatibility. = 2.8.7 - 2025-01-20 = diff --git a/webpack.config.js b/webpack.config.js index fc5abbd6f..9407b5caa 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -55,6 +55,7 @@ module.exports = { './client/provide-public-path.js', './client/main.js' ], + 'woocommerce-services-checkout': [ './client/checkout-notices/index.js' ], 'woocommerce-services-banner': [ './client/banner.js' ], 'woocommerce-services-admin-pointers': [ './client/admin-pointers.js' ], 'woocommerce-services-new-order-taxjar': [ './client/new-order-taxjar.js' ], diff --git a/woocommerce-services.php b/woocommerce-services.php index 0faf9d374..654d5c7d9 100644 --- a/woocommerce-services.php +++ b/woocommerce-services.php @@ -333,7 +333,7 @@ public static function get_wcs_version() { * * @return string */ - private static function get_wc_connect_base_url() { + public static function get_wc_connect_base_url() { return trailingslashit( defined( 'WOOCOMMERCE_CONNECT_DEV_SERVER_URL' ) ? WOOCOMMERCE_CONNECT_DEV_SERVER_URL : plugins_url( 'dist/', __FILE__ ) ); } @@ -359,6 +359,24 @@ public static function get_wcs_admin_style_url() { } } + /** + * Get plugin absolute path. + * + * @return string + */ + public static function get_wcs_abs_path() { + return __DIR__ . '/'; + } + + /** + * Get plugin distribution path. + * + * @return string + */ + public static function get_wc_connect_base_path() { + return __DIR__ . '/dist/'; + } + public function __construct() { $this->wc_connect_base_url = self::get_wc_connect_base_url(); add_action( @@ -663,6 +681,8 @@ function () { } add_action( 'before_woocommerce_init', array( $this, 'pre_wc_init' ) ); + add_action( 'woocommerce_blocks_loaded', array( $this, 'register_blocks_integration' ) ); + add_action( 'woocommerce_blocks_loaded', array( $this, 'extend_store_api' ) ); } /** @@ -896,6 +916,35 @@ public function attach_hooks() { } } + /** + * Register blocks integration. + */ + public function register_blocks_integration() { + require_once __DIR__ . '/classes/class-wc-connect-blocks-integration.php'; + + add_action( + 'woocommerce_blocks_cart_block_registration', + function ( $integration_registry ) { + $integration_registry->register( new WC_Connect_Blocks_Integration() ); + } + ); + + add_action( + 'woocommerce_blocks_checkout_block_registration', + function ( $integration_registry ) { + $integration_registry->register( new WC_Connect_Blocks_Integration() ); + } + ); + } + + /** + * Extend the store API. + */ + public function extend_store_api() { + require_once __DIR__ . '/classes/class-wc-connect-store-api-extension.php'; + WC_Connect_Store_API_Extension::init(); + } + /** * Register shipping labels-related hooks. *