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 =