From c6edada0257e9fd8daccb7b758f06762d29417d0 Mon Sep 17 00:00:00 2001 From: David Stone Date: Thu, 7 May 2026 16:50:27 -0600 Subject: [PATCH] fix(tours): fix wu_tours undefined error and ensure once-only display MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two bugs in the admin tour system: 1. wu_tours is not defined (JS ReferenceError on checkout form editor) enqueue_scripts() is hooked to in_admin_footer, but by that point the scripts (including 'underscore') have already been printed by WordPress. Calling wp_add_inline_script('underscore', ...) after the script tag has been output is silently ignored, so wu_tours and wu_tours_vars are never defined when tours.min.js executes. Fix: use wp_print_inline_script_tag() directly inside in_admin_footer. That hook fires before admin_footer, which is where WordPress prints enqueued script modules — guaranteeing wu_tours is defined first. 2. Tour re-shows after first completion (shows twice instead of once) markTourFinished() used a fire-and-forget $.ajax() call. When the user clicks Close on the last step, Shepherd removes the tour overlay immediately. If the user refreshes before the async request gets a response, the browser cancels the in-flight request and mark_as_finished never runs — the setting is never saved. Fix: use navigator.sendBeacon() which queues delivery at the OS/browser level and survives page navigation/unload. URLSearchParams payload is sent as application/x-www-form-urlencoded POST so wu_request() and check_ajax_referer() work without PHP-side changes. Falls back to $.ajax() for browsers without sendBeacon support. --- assets/js/tours.js | 32 ++++++++++++++++-------- assets/js/tours.min.js | 2 +- inc/ui/class-tours.php | 57 +++++++++++++++++++++--------------------- 3 files changed, 51 insertions(+), 40 deletions(-) diff --git a/assets/js/tours.js b/assets/js/tours.js index 661338d75..6d589f165 100644 --- a/assets/js/tours.js +++ b/assets/js/tours.js @@ -41,18 +41,28 @@ import Shepherd from 'shepherd.js'; }, }); - const markTourFinished = function() { - - $.ajax({ - url: ajaxurl, - data: { - action: 'wu_mark_tour_as_finished', - tour_id, - nonce: wu_tours_vars.nonce, - }, - }); + const markTourFinished = function() { + + const data = new URLSearchParams({ + action: 'wu_mark_tour_as_finished', + tour_id, + nonce: wu_tours_vars.nonce, + }); - }; + /* + * sendBeacon() queues the request at the OS/browser level and + * survives page navigation — so the setting is saved even if the + * user refreshes immediately after closing the tour (which would + * cancel an in-flight $.ajax() request). Falls back to $.ajax() + * for browsers without sendBeacon support. + */ + if (navigator.sendBeacon) { + navigator.sendBeacon(ajaxurl, data); + } else { + $.ajax({ url: ajaxurl, data: Object.fromEntries(data) }); + } + + }; window[ tour_id ].on('complete', markTourFinished); window[ tour_id ].on('cancel', markTourFinished); diff --git a/assets/js/tours.min.js b/assets/js/tours.min.js index b743c1cee..9411ef5b2 100644 --- a/assets/js/tours.min.js +++ b/assets/js/tours.min.js @@ -1 +1 @@ -import Shepherd from"shepherd.js";(n=>{n(document).ready(function(){_.each(wu_tours,function(e,o){window[o]=new Shepherd.Tour({useModalOverlay:!0,includeStyles:!1,styleVariables:{arrowSize:1.1},defaultStepOptions:{classes:"wu-p-2 wu-bg-white wu-shadow-sm wu-rounded wu-text-left wu-text-gray-700",scrollTo:{block:"center",behavior:"smooth"},tippyOptions:{zIndex:999999,onCreate(t){t.popper.classList.add("wu-styling");t=t.popperChildren.content.children[0].children[0].children;t[0].children[0]&&t[0].children[0].classList.add("wu-p-2","wu-pb-0","wu-m-0","wu--mb-1","wu-text-gray-800"),t[1].classList.add("wu-p-2"),t[2].classList.add("wu--mt-1","wu-p-2","wu-bg-gray-200","wu-rounded","wu-text-right")}}}});function t(){n.ajax({url:ajaxurl,data:{action:"wu_mark_tour_as_finished",tour_id:o,nonce:wu_tours_vars.nonce}})}window[o].on("complete",t),window[o].on("cancel",t),_.each(e,function(t,n){n=n+1===e.length;t.buttons=_.isArray(t.buttons)?t.buttons:[],t.buttons=_.map(t.buttons,function(t){var n,e;return t.action=([n,e="_blank"]=[t.url,t.target],()=>{window.open(n,e)}),t}),window[o].addStep({...t,buttons:[...t.buttons,{classes:"button button-primary wu-text-xs sm:wu-normal-case",text:n?wu_tours_vars.i18n.finish:wu_tours_vars.i18n.next,action:window[o].next}]})}),window[o].start()})})})(jQuery); \ No newline at end of file +import Shepherd from"shepherd.js";(n=>{n(document).ready(function(){_.each(wu_tours,function(e,o){window[o]=new Shepherd.Tour({useModalOverlay:!0,includeStyles:!1,styleVariables:{arrowSize:1.1},defaultStepOptions:{classes:"wu-p-2 wu-bg-white wu-shadow-sm wu-rounded wu-text-left wu-text-gray-700",scrollTo:{block:"center",behavior:"smooth"},tippyOptions:{zIndex:999999,onCreate(t){t.popper.classList.add("wu-styling");t=t.popperChildren.content.children[0].children[0].children;t[0].children[0]&&t[0].children[0].classList.add("wu-p-2","wu-pb-0","wu-m-0","wu--mb-1","wu-text-gray-800"),t[1].classList.add("wu-p-2"),t[2].classList.add("wu--mt-1","wu-p-2","wu-bg-gray-200","wu-rounded","wu-text-right")}}}});function t(){const e=new URLSearchParams({action:"wu_mark_tour_as_finished",tour_id:o,nonce:wu_tours_vars.nonce});navigator.sendBeacon?navigator.sendBeacon(ajaxurl,e):n.ajax({url:ajaxurl,data:Object.fromEntries(e)})}window[o].on("complete",t),window[o].on("cancel",t),_.each(e,function(t,n){n=n+1===e.length;t.buttons=_.isArray(t.buttons)?t.buttons:[],t.buttons=_.map(t.buttons,function(t){var n,e;return t.action=([n,e="_blank"]=[t.url,t.target],()=>{window.open(n,e)}),t}),window[o].addStep({...t,buttons:[...t.buttons,{classes:"button button-primary wu-text-xs sm:wu-normal-case",text:n?wu_tours_vars.i18n.finish:wu_tours_vars.i18n.next,action:window[o].next}]})}),window[o].start()})})})(jQuery); \ No newline at end of file diff --git a/inc/ui/class-tours.php b/inc/ui/class-tours.php index 7d6adc2bd..4932b597b 100644 --- a/inc/ui/class-tours.php +++ b/inc/ui/class-tours.php @@ -114,38 +114,39 @@ public function enqueue_scripts(): void { if ($this->has_tours()) { /* - * We cannot use wp_localize_script() on a module script (wu-tours), and - * we cannot rely on wu-admin being enqueued on every admin page — since - * PR #433 it is only enqueued on WP Ultimo pages. The network dashboard - * (index.php, hook suffix dashboard-network) is not a WP Ultimo page, so - * wu-admin is absent there and localizing onto it silently does nothing, - * leaving wu_tours undefined when tours.js executes. + * We cannot use wp_localize_script() on a module script (wu-tours). * - * Fix: use wp_add_inline_script() on 'underscore', which is a WordPress - * core script always present in the admin. This injects wu_tours and - * wu_tours_vars as globals immediately after underscore loads, making them - * available to the wu-tours module regardless of whether wu-admin is - * enqueued. See https://core.trac.wordpress.org/ticket/60234. + * We also cannot use wp_add_inline_script() on 'underscore' here, + * because enqueue_scripts() is hooked to in_admin_footer — by the time + * that hook fires, the scripts (including underscore) have + * already been printed, so wp_add_inline_script() is silently ignored, + * leaving wu_tours undefined when tours.min.js executes. + * + * Fix: call wp_print_inline_script_tag() directly in in_admin_footer. + * That hook fires before admin_footer, which is where WordPress prints + * enqueued script modules, so wu_tours is guaranteed to be defined + * before the wu-tours module runs. wp_print_inline_script_tag() is + * available since WP 5.7; this code path already requires WP 6.5+ + * (for wp_enqueue_script_module), so no version guard is needed. */ - wp_enqueue_script('underscore'); - - $inline_data = sprintf( - 'var wu_tours = %s; var wu_tours_vars = %s;', - wp_json_encode($this->tours), - wp_json_encode( - [ - 'ajaxurl' => wu_ajax_url(), - 'nonce' => wp_create_nonce('wu_tour_finished'), - 'i18n' => [ - 'next' => __('Next', 'ultimate-multisite'), - 'finish' => __('Close', 'ultimate-multisite'), - ], - ] - ) + wp_print_inline_script_tag( + sprintf( + 'var wu_tours = %s; var wu_tours_vars = %s;', + wp_json_encode($this->tours), + wp_json_encode( + [ + 'ajaxurl' => wu_ajax_url(), + 'nonce' => wp_create_nonce('wu_tour_finished'), + 'i18n' => [ + 'next' => __('Next', 'ultimate-multisite'), + 'finish' => __('Close', 'ultimate-multisite'), + ], + ] + ) + ), + ['id' => 'wu-tours-data'] ); - wp_add_inline_script('underscore', $inline_data, 'after'); - wp_enqueue_script_module('wu-tours'); wp_enqueue_style('shepherd'); }