diff --git a/.gitignore b/.gitignore index 955c02cea..999d83b60 100644 --- a/.gitignore +++ b/.gitignore @@ -27,6 +27,7 @@ woocommerce-services.zip dist/ .cache/ tests/e2e-tests/screenshots +tests/docker/docker-compose.override.yml # docker data for local development docker/data diff --git a/classes/class-wc-connect-nux.php b/classes/class-wc-connect-nux.php index 908530907..12d5c69b9 100644 --- a/classes/class-wc-connect-nux.php +++ b/classes/class-wc-connect-nux.php @@ -16,7 +16,8 @@ class WC_Connect_Nux { * Option name for dismissing success banner * after the JP connection flow */ - const SHOULD_SHOW_AFTER_CXN_BANNER = 'should_display_nux_after_jp_cxn_banner'; + const SHOULD_SHOW_AFTER_CXN_BANNER = 'should_display_nux_after_jp_cxn_banner'; + const SHOULD_SHOW_CONTEXTUAL_BANNER = 'should_display_nux_contextual_banner'; /** * @var WC_Connect_Tracks @@ -28,9 +29,34 @@ class WC_Connect_Nux { */ private $shipping_label; - function __construct( WC_Connect_Tracks $tracks, WC_Connect_Shipping_Label $shipping_label ) { - $this->tracks = $tracks; - $this->shipping_label = $shipping_label; + /** + * @var WC_Connect_Service_Settings_Store + */ + protected $service_settings_store; + + /** + * @var WC_Connect_Payment_Methods_Store + */ + protected $payment_methods_store; + + /** + * @var WC_Connect_Service_Schemas_Store + */ + protected $service_schemas_store; + + + function __construct( + WC_Connect_Tracks $tracks, + WC_Connect_Shipping_Label $shipping_label, + WC_Connect_Service_Settings_Store $service_settings_store, + WC_Connect_Payment_Methods_Store $payment_methods_store, + WC_Connect_Service_Schemas_Store $service_schemas_store + ) { + $this->tracks = $tracks; + $this->shipping_label = $shipping_label; + $this->service_settings_store = $service_settings_store; + $this->payment_methods_store = $payment_methods_store; + $this->service_schemas_store = $service_schemas_store; $this->init_pointers(); } @@ -241,23 +267,52 @@ public static function get_banner_type_to_display( $status = array() ) { return 'before_jetpack_connection'; case self::JETPACK_CONNECTED: case self::JETPACK_OFFLINE_MODE: - // Has the user just gone through our NUX connection flow? + $is_us_store = ( isset( $status['store_country'] ) && 'US' === $status['store_country'] ); + + // Priority 1: Standard "after connection" banner (if pending from NUX flow). + // This banner also handles initial TOS acceptance if coming from the NUX connection flow. if ( isset( $status['should_display_after_cxn_banner'] ) && $status['should_display_after_cxn_banner'] ) { return 'after_jetpack_connection'; } - // Has the user already accepted our TOS? Then do nothing. - // Note: TOS is accepted during the after_connection banner - if ( - isset( $status['tos_accepted'] ) - && ! $status['tos_accepted'] - && isset( $status['can_accept_tos'] ) - && $status['can_accept_tos'] - ) { + // Priority 2: TOS acceptance banner (if Jetpack connected, but TOS not yet accepted, + // and the standard "after connection" banner is not pending). + if ( isset( $status['tos_accepted'] ) && ! $status['tos_accepted'] && + isset( $status['can_accept_tos'] ) && $status['can_accept_tos'] ) { return 'tos_only_banner'; } - return false; + // For existing users: if TOS accepted, after_cxn_banner done, but contextual_banner flag not yet set, set it now. + if ( isset( $status['tos_accepted'] ) && $status['tos_accepted'] && + ( ! isset( $status['should_display_after_cxn_banner'] ) || ! $status['should_display_after_cxn_banner'] ) && + ( ! isset( $status['should_display_contextual_banner'] ) || ! $status['should_display_contextual_banner'] ) + ) { + // This user is eligible for contextual banners but the flag isn't set. Set it now. + WC_Connect_Options::update_option( self::SHOULD_SHOW_CONTEXTUAL_BANNER, true ); + // Update the status for the current execution path, so Priority 3 check below can pick it up. + $status['should_display_contextual_banner'] = true; + // Fallback for non-US stores if contextual banner flag is not set + if ( ! $is_us_store ) { + return 'after_cxn_non_us'; + } + } + + // Priority 3: Contextual banners (if standard "after connection" is done or was not applicable, + // TOS is accepted, and the contextual flag is set - either previously or by the block above). + if ( isset( $status['should_display_contextual_banner'] ) && $status['should_display_contextual_banner'] ) { + // Determine which specific contextual banner to show. + if ( $is_us_store ) { + if ( isset( $status['is_wcs_shipping_plugin_active'] ) && ! $status['is_wcs_shipping_plugin_active'] ) { + return 'after_cxn_us_no_wcs_plugin'; + } else { + return 'after_cxn_us_with_wcs_plugin'; + } + } else { + return 'after_cxn_non_us'; + } + } + + return false; // All NUX banners handled or no NUX banner needed for this state. default: return false; } @@ -372,12 +427,22 @@ public function set_up_nux_notices() { // If this is the case, the admin can connect the site on their own, and should be able to use WCS as ususal $jetpack_install_status = $this->get_jetpack_install_status(); + // Ensure is_plugin_active() is available for WCS check + if ( ! function_exists( 'is_plugin_active' ) ) { + include_once ABSPATH . 'wp-admin/includes/plugin.php'; + } + $is_wcs_shipping_plugin_active = is_plugin_active( 'woocommerce-shipping/woocommerce-shipping.php' ); + $store_country = WC()->countries->get_base_country(); + $banner_to_display = self::get_banner_type_to_display( array( - 'jetpack_connection_status' => $jetpack_install_status, - 'tos_accepted' => WC_Connect_Options::get_option( 'tos_accepted' ), - 'can_accept_tos' => WC_Connect_Jetpack::is_current_user_connection_owner() || WC_Connect_Jetpack::is_offline_mode(), - 'should_display_after_cxn_banner' => WC_Connect_Options::get_option( self::SHOULD_SHOW_AFTER_CXN_BANNER ), + 'jetpack_connection_status' => $jetpack_install_status, + 'tos_accepted' => WC_Connect_Options::get_option( 'tos_accepted' ), + 'can_accept_tos' => WC_Connect_Jetpack::is_current_user_connection_owner() || WC_Connect_Jetpack::is_offline_mode(), + 'should_display_after_cxn_banner' => WC_Connect_Options::get_option( self::SHOULD_SHOW_AFTER_CXN_BANNER ), + 'should_display_contextual_banner' => WC_Connect_Options::get_option( self::SHOULD_SHOW_CONTEXTUAL_BANNER ), + 'store_country' => $store_country, + 'is_wcs_shipping_plugin_active' => $is_wcs_shipping_plugin_active, ) ); @@ -391,13 +456,87 @@ public function set_up_nux_notices() { wp_enqueue_style( 'wc_connect_banner' ); add_action( 'admin_notices', array( $this, 'show_banner_before_connection' ), 9 ); break; + case 'after_jetpack_connection': + wp_enqueue_style( 'wc_connect_banner' ); + add_action( 'admin_notices', array( $this, 'show_banner_after_connection' ) ); + break; case 'tos_only_banner': wp_enqueue_style( 'wc_connect_banner' ); add_action( 'admin_notices', array( $this, 'show_tos_banner' ) ); break; - case 'after_jetpack_connection': + case 'after_cxn_us_no_wcs_plugin': + // Enqueue the migration modal assets specifically for this banner on the plugins page. + $plugin_version = WC_Connect_Loader::get_wcs_version(); + // Use the public static method from WC_Connect_Loader + $base_url = WC_Connect_Loader::get_wc_connect_base_url(); // Assuming get_wc_connect_base_url is static + + wp_register_style( 'wcst_wcshipping_migration_admin_notice', $base_url . 'woocommerce-services-wcshipping-migration-admin-notice-' . $plugin_version . '.css', array() ); // phpcs:ignore WordPress.WP.EnqueuedResourceParameters.MissingVersion + // Add 'wp-element' and 'wc_connect_admin' dependency for React and base script + wp_register_script( 'wcst_wcshipping_migration_admin_notice', $base_url . 'woocommerce-services-wcshipping-migration-admin-notice-' . $plugin_version . '.js', array( 'wc_connect_admin', 'wp-element' ), false, true ); // phpcs:ignore WordPress.WP.EnqueuedResourceParameters.MissingVersion + + // Localize script data - MATCHING the original register_wcshipping_migration_modal + // Note: The modal primarily uses data-args, localization is minimal here. + wp_localize_script( + 'wcst_wcshipping_migration_admin_notice', + 'wcsPluginData', // Ensure this matches the expected object name in the script + array( + 'assetPath' => $base_url, + 'adminPluginPath' => admin_url( 'plugins.php' ), + ) + ); + + // Enqueue scripts/styles needed for the banner and modal + wp_enqueue_script( 'wc_connect_admin' ); // Ensure base script is loaded first + wp_enqueue_script( 'wcst_wcshipping_migration_admin_notice' ); + wp_enqueue_style( 'wcst_wcshipping_migration_admin_notice' ); wp_enqueue_style( 'wc_connect_banner' ); - add_action( 'admin_notices', array( $this, 'show_banner_after_connection' ) ); + + // Add the action to render the notice and container div + add_action( + 'admin_notices', + function () use ( $banner_to_display ) { + // Instantiate settings classes HERE, inside the closure, using stored dependencies + $account_settings = new WC_Connect_Account_Settings( + $this->service_settings_store, + $this->payment_methods_store + ); + $packages_settings = new WC_Connect_Package_Settings( + $this->service_settings_store, + $this->service_schemas_store + ); + + // Prepare the data for the data-args attribute by calling get() + $container_data_args = array( + 'nonce' => wp_create_nonce( 'wp_rest' ), + 'baseURL' => get_rest_url(), + 'accountSettings' => $account_settings->get(), // Get REAL data + 'packagesSettings' => $packages_settings->get(), // Get REAL data + ); + $encoded_container_args = wp_json_encode( $container_data_args ); + + // Echo the container div needed for the modal React component BEFORE the banner. + // Add 'display: none;' initially; the script should manage visibility. + printf( + '
', + esc_attr( $encoded_container_args ) // Use the REAL encoded data + ); + + // Now show the banner itself + $this->show_contextual_after_connection_banner( $banner_to_display ); + } + ); + break; // End case 'after_cxn_us_no_wcs_plugin' + + case 'after_cxn_us_with_wcs_plugin': + case 'after_cxn_non_us': + wp_enqueue_style( 'wc_connect_banner' ); + // Using a closure to correctly pass the argument to the new handler method. + add_action( + 'admin_notices', + function () use ( $banner_to_display ) { + $this->show_contextual_after_connection_banner( $banner_to_display ); + } + ); break; } @@ -405,6 +544,10 @@ public function set_up_nux_notices() { } public function show_banner_before_connection() { + if ( get_option( 'wcs_nux_any_banner_shown', false ) ) { + return; + } + if ( ! $this->should_display_nux_notice_for_current_store_locale() ) { return; } @@ -442,6 +585,10 @@ public function show_banner_before_connection() { } public function show_banner_after_connection() { + if ( get_option( 'wcs_nux_any_banner_shown', false ) ) { + return; + } + if ( ! $this->should_display_nux_notice_for_current_store_locale() ) { return; } @@ -452,8 +599,11 @@ public function show_banner_after_connection() { // Did the user just dismiss? if ( isset( $_GET['wcs-nux-notice'] ) && 'dismiss' === $_GET['wcs-nux-notice'] ) { - // No longer need to keep track of whether the before connection banner was displayed. + // Delete the flag for this banner WC_Connect_Options::delete_option( self::SHOULD_SHOW_AFTER_CXN_BANNER ); + // Set the flag for the next contextual banner + WC_Connect_Options::update_option( self::SHOULD_SHOW_CONTEXTUAL_BANNER, true ); + delete_option( 'wcs_nux_any_banner_shown' ); wp_safe_redirect( remove_query_arg( 'wcs-nux-notice' ) ); exit; } @@ -487,7 +637,95 @@ public function show_banner_after_connection() { ); } + public function show_contextual_after_connection_banner( $banner_type ) { + if ( get_option( 'wcs_nux_any_banner_shown', false ) ) { + return; + } + + $screen = get_current_screen(); + + // This specific banner should only appear on the plugins page. + if ( ! $screen || 'plugins' !== $screen->base ) { + return; + } + + // Still respect the store locale check. + if ( ! $this->should_display_nux_notice_for_current_store_locale() ) { + return; + } + + // Did the user just dismiss? + if ( isset( $_GET['wcs-nux-notice'] ) && 'dismiss' === $_GET['wcs-nux-notice'] ) { + // Delete the flag for this contextual banner + WC_Connect_Options::delete_option( self::SHOULD_SHOW_CONTEXTUAL_BANNER ); + delete_option( 'wcs_nux_any_banner_shown' ); + wp_safe_redirect( remove_query_arg( 'wcs-nux-notice' ) ); + exit; + } + + // By going through the connection process, the user has accepted our TOS + WC_Connect_Options::update_option( 'tos_accepted', true ); + + // Using a generic tracks event, can be made more specific if needed. + $this->tracks->opted_in( 'contextual_connection_banner_viewed' ); + + update_option( 'wcs_nux_any_banner_shown', true ); + + $banner_title = ''; + $banner_description = ''; + $banner_button_text = ''; + $banner_button_link = null; + + update_option( 'wcshipping_migration_state', '0' ); + switch ( $banner_type ) { + case 'after_cxn_us_no_wcs_plugin': + $banner_title = __( 'WooCommerce Shipping & Tax has been renamed to WooCommerce Tax', 'woocommerce-services' ); + $banner_description = __( 'Your tax functionality will continue to work as expected. The shipping functionality in this plugin will be discontinued on September 1, 2025. Please migrate to the new WooCommerce Shipping extension to get discounted labels for UPS, USPS, DHL Express— and more coming soon!', 'woocommerce-services' ); + $banner_button_text = __( 'Try WooCommerce Shipping ', 'woocommerce-services' ); + // Ensure this line uses the special trigger value: + $banner_button_link = '#trigger-migration-modal'; + break; + case 'after_cxn_us_with_wcs_plugin': + $banner_title = __( 'WooCommerce Shipping & Tax has been renamed to WooCommerce Tax', 'woocommerce-services' ); + $banner_description = __( 'Your tax functionality will continue to work as expected. Use WooCommerce Shipping to access deeply discounted UPS, USPS, and DHL shipping labels, reliable shipments, and on-time delivery options.', 'woocommerce-services' ); + $banner_button_text = __( 'Ship with UPS on WooCommerce', 'woocommerce-services' ); + $banner_button_link = 'https://woocommerce.com/document/woocommerce-shipping/#creating-shipping-labels'; + break; + case 'after_cxn_non_us': + $banner_title = __( 'WooCommerce Shipping & Tax has been renamed to WooCommerce Tax', 'woocommerce-services' ); + $banner_description = __( 'Your tax functionality will continue to work as expected. No action is required.', 'woocommerce-services' ); + $banner_button_text = __( 'Close', 'woocommerce-services' ); + $banner_button_link = add_query_arg( + array( + 'wcs-nux-notice' => 'dismiss', + ) + ); + break; + default: + $this->tracks->opted_in( 'contextual_connection_banner_viewed_unknown' ); + return; + } + + $this->show_nux_banner( + array( + 'title' => $banner_title, + 'description' => esc_html( $banner_description ), + 'button_text' => $banner_button_text, + 'button_link' => $banner_button_link, + 'image_url' => plugins_url( + 'images/wcs-notice.png', + __DIR__ + ), + 'should_show_terms' => false, + ) + ); + } + public function show_tos_banner() { + if ( get_option( 'wcs_nux_any_banner_shown', false ) ) { + return; + } + if ( ! $this->should_display_nux_notice_for_current_store_locale() ) { return; } @@ -498,9 +736,13 @@ public function show_tos_banner() { if ( isset( $_GET['wcs-nux-tos'] ) && 'accept' === $_GET['wcs-nux-tos'] ) { WC_Connect_Options::update_option( 'tos_accepted', true ); + // Signal that the contextual banner can now be shown + WC_Connect_Options::update_option( self::SHOULD_SHOW_CONTEXTUAL_BANNER, true ); $this->tracks->opted_in( 'tos_banner' ); + delete_option( 'wcs_nux_any_banner_shown' ); + wp_safe_redirect( remove_query_arg( 'wcs-nux-tos' ) ); exit; } @@ -567,12 +809,23 @@ public function show_nux_banner( $content ) { - - - + + + + + + + +