fix(tours): persist tour-finished flag in user meta to avoid wp-settings cookie desync#1268
Conversation
…ngs cookie desync
The checkout-form editor tour (and any other WU tour dismissed via the
AJAX `wu_mark_tour_as_finished` handler) re-appeared on every visit even
after the user clicked through and closed it. Reproducible at
`wp-admin/network/admin.php?page=wp-ultimo-edit-checkout-form&id=1&slug=main-form&model=checkout_form`:
complete the tour, click "Save Checkout Form", revisit the page, the
tour reappears.
Root cause: `Tours::mark_as_finished()` writes the dismissal flag with
`set_user_setting()`, which stores it both in the `wp_user-settings`
user meta and in the per-user `wp-settings-{uid}` cookie. In modern
WordPress, the cookie is only synced from user meta by
`wp_user_settings()` on regular admin page loads — that function
short-circuits during AJAX. The AJAX dismissal therefore never sends a
`Set-Cookie` header, so the browser keeps a stale (or missing)
`wp-settings-{uid}` cookie, `get_user_setting()` returns false on the
next page, and the closure registered in `Tours::create_tour()`
re-renders the tour.
Fix: persist the finished flag in a dedicated user meta key
`wu_tour_finished_{snake_id}` from the AJAX handler. Add
`Tours::is_tour_finished()` which reads that meta first and falls back
to the legacy `get_user_setting()` for users who dismissed tours before
this release. `create_tour()` now consults `is_tour_finished()` instead
of `get_user_setting()` directly. The legacy `set_user_setting()` write
is retained for backward compatibility.
Verification:
- New unit tests cover `get_meta_key()`, the user-meta read path, and
the legacy cookie fallback (`vendor/bin/phpunit --filter Tours_Test`).
- Browser repro on the affected page (admin/admin against the local
WP 7.0-RC2 dev install): with state cleared, complete the tour,
click "Save Checkout Form", revisit — `window.wu_tours` is now
`undefined` and no `.shepherd-element` is rendered; `wp_usermeta`
contains `wu_tour_finished_checkout_form_editor = 1`.
`vendor/bin/phpcs inc/ui/class-tours.php` clean; pre-existing
`NoExplicitVersion` warning on `wp_register_script` (line 339) is
unrelated to this change. `vendor/bin/phpstan analyse inc/ui/class-tours.php`
clean.
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (2)
📝 WalkthroughWalkthroughThe Tours UI now persists tour completion state in user meta instead of relying solely on WordPress user-settings cookies. A new user-meta key is computed from the tour ID with prefix and hyphen normalization. Tour finished checks read user meta first, falling back to legacy user settings for backward compatibility. The AJAX handler writes completed flags to user meta before the legacy cookie path, and tour display logic uses the new finished-state helper. ChangesTour Finished State Persistence
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related PRs
Suggested labels
Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
🔨 Build Complete - Ready for Testing!📦 Download Build Artifact (Recommended)Download the zip build, upload to WordPress and test:
🌐 Test in WordPress Playground (Very Experimental)Click the link below to instantly test this PR in your browser - no installation needed! Login credentials: |
|
Performance Test Results Performance test results for c5f78d5 are in 🛎️! Note: the numbers in parentheses show the difference to the previous (baseline) test run. Differences below 2% or 0.5 in absolute values are not shown. URL:
|
SummaryFixes the Ultimate Multisite tour for the checkout-form editor (and any Root cause
FixPersist the finished flag in a dedicated user-meta key
Verification
Compatibility
aidevops.sh v3.18.1 plugin for OpenCode v1.15.10 with claude-opus-4-7 spent 2d 12h and 54 tokens on this as a headless worker. Merged via PR #1268 to main. |
|
Permission check failed for this PR (HTTP unknown from collaborator permission API). Unable to determine if @superdav42 is a maintainer or external contributor. A maintainer must review and merge this PR manually. This is a fail-closed safety measure — the pulse will not auto-merge until the permission API succeeds. aidevops.sh v3.19.1 plugin for OpenCode v1.15.10 with gpt-5.5 spent 1m and 39,362 tokens on this as a headless worker. |
…peating on every page load (#1281) * fix(tours): mark one-shot tours as finished on render to stop them repeating on every page load When a tour was rendered without the user explicitly clicking through to the last step (e.g. they refreshed the page, navigated away mid-walkthrough, closed the browser tab, or hit the back button before reaching the close button), the wu_tour_finished_* meta flag was never written. The next page load would re-render the same tour, producing the user-visible symptom of "the same admin tour keeps appearing on every page load". Previous fixes (#1051, #1268, #1271, #1277) ensured the dismissal *could* persist across cookie / id-normalization / user-scoping edge cases — but all of them still depended on the AJAX dismissal triggered by Shepherd's complete / cancel events. If the user never reached the end of the tour, no AJAX call was made and no flag was stored. Persist the finished flag synchronously, server-side, the moment a one-shot tour is queued for display. The Shepherd event handlers in tours.js still fire markTourFinished for completeness; update_user_meta is idempotent so the double-write is harmless. Tours registered with $once = false continue to render on every page load. Verified on https://ruling-sable.jurassic.ninja (Ultimate Multisite v2.12.0 deploy) by: - Resetting wu_tour_* user meta and wp_user-settings for the demo user in a brand new agent-browser session. - Loading /wp-admin/network/admin.php?page=wp-ultimo — tour renders once, wu_tour_finished_wp_ultimo_dashboard = 1 written immediately. - Reloading the same URL — tour no longer renders. - Repeating for /wp-admin/network/admin.php?page=wp-ultimo-checkout-forms (checkout-form-list) — same one-shot behaviour confirmed. * fix(tours): avoid rewriting filter-forced tour state * fix(e2e): stabilize Cypress login and password reset fixture
Summary
Fixes the Ultimate Multisite tour for the checkout-form editor (and any
other WU tour dismissed via AJAX) re-appearing on every visit.
Reproducible at
wp-admin/network/admin.php?page=wp-ultimo-edit-checkout-form&id=1&slug=main-form&model=checkout_form:complete the tour, click Save Checkout Form, revisit the page — the
tour shows again.
Why this isn't a duplicate of prior tour fixes
Three earlier PRs touched the same area — each fixed an adjacent
failure mode but left the underlying cookie-sync gap intact:
a3e62958)set_user_setting()06aa90a8)checkout-form-editor) silently corrupted thewp_user-settingsstring — addedget_setting_key()to normalise-→_99abf629, May 2026)wu_toursJS global undefined (script-order race); (b)$.ajax()dismissal killed by page navigation — switched tonavigator.sendBeacon()so the request reliably reaches the serverset_user_setting()writes to user-meta, butwp_user_settings()short-circuits on AJAX so the browser cookie never updates andget_user_setting()still reads stale$_COOKIEon the next requestThis PR is the server-side persistence layer: once the dismissal reaches
the server (now reliable thanks to #1161), we must read it back without
depending on a cookie that AJAX cannot set.
Root cause
Tours::mark_as_finished()only wrote the dismissal flag viaset_user_setting(), which stores the value in thewp_user-settingsuser-meta string and in the per-user
wp-settings-{uid}cookie. Inmodern WordPress, the cookie is only synced from user meta by
wp_user_settings()— and that function short-circuits during AJAXrequests. Because the dismissal is sent through the AJAX action
wu_mark_tour_as_finished, noSet-Cookieheader is ever emitted, so:wp-settings-{uid}cookie.get_user_setting()reads$_COOKIEandreturns
false.Tours::create_tour()re-renders thetour.
Fix
Persist the finished flag in a dedicated user-meta key
wu_tour_finished_{snake_id}from the AJAX handler. The closure increate_tour()now calls a newis_tour_finished()helper thatreads:
get_user_meta($user_id, 'wu_tour_finished_{snake_id}', true)—the new, cookie-independent source of truth.
get_user_setting()— legacy fallback so users who dismissed atour before this release are not re-prompted.
The legacy
set_user_setting()write is retained for backwardcompatibility with downstream code that may read the older key.
Verification
tests/WP_Ultimo/UI/Tours_Test.php:test_get_meta_key_uses_wu_tour_finished_prefixtest_is_tour_finished_reads_user_metatest_is_tour_finished_falls_back_to_legacy_user_settingvendor/bin/phpunit --filter Tours_Test→ all new tests pass.(The pre-existing failure
test_enqueue_scripts_inlines_data_on_underscore_not_wu_adminreproduces on unmodified
mainand is unrelated to this change.)vendor/bin/phpcs inc/ui/class-tours.phpclean; pre-existingNoExplicitVersionwarning onwp_register_script(line 339 of thetest file) is unrelated.
vendor/bin/phpstan analyse inc/ui/class-tours.phpclean.state cleared, completed the tour, clicked Save Checkout Form,
revisited the page —
window.wu_toursis nowundefined, no.shepherd-elementis rendered, andwp_usermetacontainswu_tour_finished_checkout_form_editor = 1.Compatibility
set_user_setting()write retained.protected; no public API change.wp_usermeta).