PluginProbe
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin / 1.0.1
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin v1.0.1
1.1.10 1.1.9 1.1.8 1.1.7 1.1.6 1.1.5 1.1.4 1.1.3 1.1.2 1.1.1 1.1.0 1.0.1 1.0.0 0.9.8 0.9.7 0.9.6 0.9.4 0.9.5 0.9.3 0.9.2 0.9.1 0.9.0 0.8.9 0.8.8 0.8.7 All 34 releases
desktop-mode / includes / welcome-dialog.php

welcome-dialog.php in OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin 1.0.1, at includes/welcome-dialog.php

754 lines 22.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * OpenStation — First-run welcome dialog.
4 *
5 * Renders a one-time, self-contained modal inside the *classic* WordPress
6 * admin (never inside the desktop shell or a chromeless iframe) telling
7 * the user where the "Switch to OpenStation" button lives in the admin
8 * bar. Dismissal is persisted via the existing seen-intros registry
9 * (`desktop_mode_seen_intros` user meta, slug `activation-welcome`),
10 * which means the "Reset what's-new dialogs" button in OpenStation Preferences →
11 * Features brings it back exactly like every other intro dialog.
12 *
13 * The dialog is intentionally self-contained — all HTML, CSS and JS are
14 * inlined into `admin_footer`. We deliberately do NOT use any of the
15 * `<os-*>` shell components here because they only ship inside the
16 * desktop bundle, which is precisely *not* loaded on the classic admin
17 * screens where this dialog is allowed to appear.
18 *
19 * @package OpenStation
20 */
21
22 defined( 'ABSPATH' ) || exit;
23
24 /** Slug stored in `desktop_mode_seen_intros` for this dialog. */
25 const OPENSTATION_WELCOME_INTRO_SLUG = 'activation-welcome';
26
27 /**
28 * Decides whether the welcome dialog should render on the current request.
29 *
30 * Six gates:
31 *
32 * 1. We're inside `/wp-admin` (`is_admin()`).
33 * 2. The user is logged in and can `read` (sanity gate — the dialog has
34 * no destructive surface, but anonymous output makes no sense).
35 * 3. The request is NOT chromeless — chromeless pages are iframes
36 * rendering inside the desktop shell; the parent shell already shows
37 * its own UX.
38 * 4. OpenStation is NOT already enabled for the user. This is a
39 * "switch to OpenStation" promo, so it has nothing to say once the
40 * user is in the shell. The desktop shell's *parent* page is admin
41 * context and is not chromeless, so without this gate the dialog
42 * re-renders there the moment the user clicks "Enable it now" — read
43 * as a duplicate dialog, because the fire-and-forget seen-intro POST
44 * races the redirect into the shell and often loses.
45 * 5. The user has not already dismissed this intro.
46 * 6. The `openstation_show_welcome_dialog` filter returns truthy, so
47 * sites can suppress the dialog entirely (e.g. managed-host onboarding
48 * flows that ship their own).
49 *
50 * @return bool
51 */
52 function openstation_should_show_welcome_dialog() {
53 if ( ! is_admin() || ! is_user_logged_in() ) {
54 return false;
55 }
56 if ( ! current_user_can( 'read' ) ) {
57 return false;
58 }
59 if ( function_exists( 'openstation_is_chromeless_request' ) && openstation_is_chromeless_request() ) {
60 return false;
61 }
62 if ( function_exists( 'openstation_is_enabled' ) && openstation_is_enabled() ) {
63 return false;
64 }
65 $user_id = get_current_user_id();
66 if ( openstation_has_seen_intro( $user_id, OPENSTATION_WELCOME_INTRO_SLUG ) ) {
67 return false;
68 }
69
70 /**
71 * Filters whether the first-run welcome dialog should render for
72 * the current user on the current request. All earlier gates
73 * (admin context, capability, chromeless, seen-state) have
74 * already passed by the time this filter fires.
75 *
76 * @param bool $show Whether to render the dialog. Default true.
77 * @param int $user_id Current user ID.
78 */
79 return (bool) apply_filters( 'openstation_show_welcome_dialog', true, $user_id );
80 }
81
82 /**
83 * Prints the welcome dialog markup, styles and dismiss script into
84 * `admin_footer`.
85 *
86 * Self-contained on purpose: everything is scoped under the
87 * `.os-welcome` namespace so it cannot collide with the host
88 * admin theme. The dismiss button POSTs to the seen-intros REST route,
89 * which is exactly the same endpoint the in-shell intros use.
90 */
91 function openstation_render_welcome_dialog() {
92 if ( ! openstation_should_show_welcome_dialog() ) {
93 return;
94 }
95
96 $rest_url = esc_url_raw( rest_url( 'desktop-mode/v1/intros/seen' ) );
97 $rest_nonce = wp_create_nonce( 'wp_rest' );
98 $ajax_url = esc_url_raw( admin_url( 'admin-ajax.php' ) );
99 $ajax_nonce = wp_create_nonce( 'save-openstation' );
100 $slug = OPENSTATION_WELCOME_INTRO_SLUG;
101
102 // All user-facing strings are passed through translation; the dialog
103 // is keyboard-dismissible (Escape) and moves initial focus to the
104 // primary CTA.
105 $title = __( 'Welcome to OpenStation', 'desktop-mode' );
106 $subtitle = __( 'A floating, window-based workspace for the WordPress admin — more like a real OS, less like a webpage.', 'desktop-mode' );
107 $body = __( 'OpenStation reimagines the WordPress admin as a true desktop environment. Open multiple admin screens side-by-side, drag and drop content between windows, and keep your work in view as you move between tasks.', 'desktop-mode' );
108 $cta = __( 'Got it', 'desktop-mode' );
109 $enable = __( 'Enable it now', 'desktop-mode' );
110 $enabling = __( 'Enabling…', 'desktop-mode' );
111
112 $features = array(
113 array(
114 'icon' => '🪟',
115 'title' => __( 'Multi-window workspace', 'desktop-mode' ),
116 'desc' => __( 'Open Posts, Media and Plugins in their own draggable, resizable windows. Tile, cascade or stack them however you work best.', 'desktop-mode' ),
117 ),
118 array(
119 'icon' => '',
120 'title' => __( 'Native, table-driven apps', 'desktop-mode' ),
121 'desc' => __( 'Posts, Pages, Users and Plugins are reimplemented as fast native windows with sticky headers, bulk actions, multi-select and inline previews.', 'desktop-mode' ),
122 ),
123 array(
124 'icon' => '🎨',
125 'title' => __( 'Wallpapers, dock & themes', 'desktop-mode' ),
126 'desc' => __( 'Make it yours. Choose a wallpaper, pin your favorite screens to the dock, and switch accent colors — all from OpenStation Preferences.', 'desktop-mode' ),
127 ),
128 array(
129 'icon' => '🤖',
130 'title' => __( 'AI Copilot, built in', 'desktop-mode' ),
131 'desc' => __( 'Press ⌘K anywhere to ask the AI Assistant — it can run commands, navigate windows and answer questions about your site.', 'desktop-mode' ),
132 ),
133 );
134 ?>
135 <style id="os-welcome-style">
136 .os-welcome,
137 .os-welcome * {
138 box-sizing: border-box;
139 }
140 .os-welcome {
141 position: fixed;
142 inset: 0;
143 z-index: 100000;
144 display: flex;
145 align-items: center;
146 justify-content: center;
147 padding: 24px;
148 background: radial-gradient(
149 ellipse at 30% 20%,
150 rgba( 236, 155, 255, 0.32 ),
151 transparent 60%
152 ),
153 radial-gradient(
154 ellipse at 70% 80%,
155 rgba( 147, 240, 198, 0.26 ),
156 transparent 55%
157 ),
158 rgba( 12, 11, 15, 0.62 );
159 backdrop-filter: blur( 14px ) saturate( 140% );
160 -webkit-backdrop-filter: blur( 14px ) saturate( 140% );
161 animation: os-welcome-fade 360ms cubic-bezier( 0.22, 1, 0.36, 1 );
162 font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
163 "Helvetica Neue", Arial, sans-serif;
164 -webkit-font-smoothing: antialiased;
165 -moz-osx-font-smoothing: grayscale;
166 }
167 @keyframes os-welcome-fade {
168 from { opacity: 0; }
169 to { opacity: 1; }
170 }
171 @keyframes os-welcome-pop {
172 0% { opacity: 0; transform: translateY( 14px ) scale( 0.96 ); }
173 100% { opacity: 1; transform: translateY( 0 ) scale( 1 ); }
174 }
175 @keyframes os-welcome-arrow {
176 0%, 100% { transform: translateX( 0 ); opacity: 0.85; }
177 50% { transform: translateX( 6px ); opacity: 1; }
178 }
179 @keyframes os-welcome-shimmer {
180 0% { background-position: 0% 50%; }
181 100% { background-position: 100% 50%; }
182 }
183 .os-welcome__card {
184 position: relative;
185 width: 100%;
186 max-width: 760px;
187 border-radius: 22px;
188 overflow: hidden;
189 background: linear-gradient(
190 145deg,
191 rgba( 255, 255, 255, 0.98 ) 0%,
192 rgba( 255, 251, 255, 0.98 ) 100%
193 );
194 box-shadow:
195 0 1px 0 rgba( 255, 255, 255, 0.85 ) inset,
196 0 30px 60px -20px rgba( 26, 23, 33, 0.55 ),
197 0 18px 36px -18px rgba( 242, 82, 252, 0.45 );
198 animation: os-welcome-pop 460ms cubic-bezier( 0.22, 1, 0.36, 1 ) both;
199 animation-delay: 60ms;
200 }
201 .os-welcome__card::before {
202 content: "";
203 position: absolute;
204 inset: -1px;
205 border-radius: inherit;
206 padding: 1px;
207 background: linear-gradient(
208 135deg,
209 rgba( 242, 82, 252, 0.65 ),
210 rgba( 236, 155, 255, 0.55 ),
211 rgba( 147, 240, 198, 0.55 )
212 );
213 -webkit-mask: linear-gradient( #000 0 0 ) content-box,
214 linear-gradient( #000 0 0 );
215 -webkit-mask-composite: xor;
216 mask-composite: exclude;
217 pointer-events: none;
218 }
219 .os-welcome__hero {
220 position: relative;
221 padding: 36px 36px 24px;
222 background: linear-gradient(
223 135deg,
224 #0c0b0f 0%,
225 #2a2533 45%,
226 #ec9bff 100%
227 );
228 background-size: 200% 200%;
229 animation: os-welcome-shimmer 14s ease infinite alternate;
230 color: #fff;
231 overflow: hidden;
232 }
233 .os-welcome__hero::after {
234 content: "";
235 position: absolute;
236 inset: 0;
237 background:
238 radial-gradient( circle at 85% 15%, rgba( 236, 155, 255, 0.35 ), transparent 45% ),
239 radial-gradient( circle at 15% 90%, rgba( 147, 240, 198, 0.22 ), transparent 50% ),
240 linear-gradient( rgba( 255, 255, 255, 0.05 ) 1px, transparent 1px ) 0 0 / 28px 28px,
241 linear-gradient( 90deg, rgba( 255, 255, 255, 0.05 ) 1px, transparent 1px ) 0 0 / 28px 28px;
242 pointer-events: none;
243 mask-image: radial-gradient( ellipse at 50% 40%, #000 35%, transparent 80% );
244 -webkit-mask-image: radial-gradient( ellipse at 50% 40%, #000 35%, transparent 80% );
245 }
246 .os-welcome__eyebrow {
247 display: inline-flex;
248 align-items: center;
249 gap: 8px;
250 padding: 5px 12px;
251 font-size: 11px;
252 font-weight: 600;
253 letter-spacing: 0.12em;
254 text-transform: uppercase;
255 color: #fff;
256 background: rgba( 255, 255, 255, 0.18 );
257 border: 1px solid rgba( 255, 255, 255, 0.28 );
258 border-radius: 999px;
259 backdrop-filter: blur( 6px );
260 -webkit-backdrop-filter: blur( 6px );
261 }
262 .os-welcome__eyebrow-dot {
263 width: 6px;
264 height: 6px;
265 border-radius: 50%;
266 background: #ec9bff;
267 box-shadow: 0 0 0 4px rgba( 236, 155, 255, 0.28 );
268 }
269 .os-welcome__title {
270 margin: 14px 0 6px;
271 font-size: 26px;
272 line-height: 1.2;
273 font-weight: 700;
274 letter-spacing: -0.01em;
275 color: #fff;
276 }
277 .os-welcome__subtitle {
278 margin: 0;
279 font-size: 14px;
280 line-height: 1.5;
281 color: rgba( 255, 255, 255, 0.85 );
282 }
283 .os-welcome__body {
284 padding: 24px 36px 24px;
285 font-size: 14.5px;
286 line-height: 1.6;
287 color: #1a1721;
288 }
289 .os-welcome__lede {
290 margin: 0;
291 font-size: 14.5px;
292 color: #33303a;
293 }
294 .os-welcome__features {
295 display: grid;
296 grid-template-columns: repeat( 2, minmax( 0, 1fr ) );
297 gap: 14px;
298 margin: 20px 0 0;
299 padding: 0;
300 list-style: none;
301 }
302 .os-welcome__feature {
303 position: relative;
304 padding: 14px 14px 14px 52px;
305 background: linear-gradient(
306 145deg,
307 rgba( 255, 255, 255, 0.85 ),
308 rgba( 255, 251, 255, 0.85 )
309 );
310 border: 1px solid rgba( 242, 82, 252, 0.18 );
311 border-radius: 12px;
312 box-shadow: 0 1px 0 rgba( 255, 255, 255, 0.9 ) inset,
313 0 2px 6px -2px rgba( 26, 23, 33, 0.08 );
314 }
315 .os-welcome__feature-icon {
316 position: absolute;
317 top: 12px;
318 left: 12px;
319 width: 30px;
320 height: 30px;
321 display: inline-flex;
322 align-items: center;
323 justify-content: center;
324 border-radius: 9px;
325 background: linear-gradient( 135deg, rgba( 242, 82, 252, 0.16 ), rgba( 147, 240, 198, 0.16 ) );
326 border: 1px solid rgba( 242, 82, 252, 0.22 );
327 font-size: 16px;
328 line-height: 1;
329 }
330 .os-welcome__feature-title {
331 display: block;
332 margin: 0 0 2px;
333 font-size: 13.5px;
334 font-weight: 700;
335 color: #0c0b0f;
336 }
337 .os-welcome__feature-desc {
338 margin: 0;
339 font-size: 12.5px;
340 line-height: 1.5;
341 color: #b3afb5;
342 }
343 .os-welcome__hint {
344 display: flex;
345 align-items: center;
346 gap: 12px;
347 margin: 18px 0 0;
348 padding: 14px 16px;
349 background: linear-gradient(
350 135deg,
351 rgba( 242, 82, 252, 0.08 ),
352 rgba( 147, 240, 198, 0.08 )
353 );
354 border: 1px solid rgba( 242, 82, 252, 0.22 );
355 border-radius: 12px;
356 font-size: 13.5px;
357 color: #0c0b0f;
358 }
359 .os-welcome__hint-icon {
360 flex-shrink: 0;
361 width: 24px;
362 height: 24px;
363 border-radius: 50%;
364 display: inline-flex;
365 align-items: center;
366 justify-content: center;
367 background: linear-gradient( 135deg, #f252fc, #ec9bff );
368 color: #fff;
369 font-size: 14px;
370 line-height: 1;
371 animation: os-welcome-arrow 1.8s ease-in-out infinite;
372 }
373 .os-welcome__hint-icon::before {
374 content: "";
375 font-weight: 700;
376 }
377 .os-welcome__hint code {
378 display: inline-block;
379 padding: 2px 6px;
380 margin: 0 2px;
381 background: rgba( 255, 255, 255, 0.85 );
382 border: 1px solid rgba( 242, 82, 252, 0.28 );
383 border-radius: 5px;
384 font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
385 font-size: 12px;
386 color: #2a2533;
387 }
388 .os-welcome__actions {
389 display: flex;
390 align-items: center;
391 justify-content: flex-end;
392 gap: 10px;
393 padding: 0 36px 28px;
394 }
395 .os-welcome__btn {
396 display: inline-flex;
397 align-items: center;
398 justify-content: center;
399 gap: 6px;
400 min-height: 38px;
401 padding: 8px 18px;
402 font-size: 14px;
403 font-weight: 600;
404 font-family: inherit;
405 line-height: 1;
406 border-radius: 10px;
407 border: 1px solid transparent;
408 cursor: pointer;
409 transition: transform 120ms ease, box-shadow 120ms ease,
410 background 120ms ease, color 120ms ease;
411 }
412 .os-welcome__btn:focus-visible {
413 outline: 2px solid #f252fc;
414 outline-offset: 2px;
415 }
416 .os-welcome__btn--ghost {
417 background: transparent;
418 color: #b3afb5;
419 border-color: transparent;
420 }
421 .os-welcome__btn--ghost:hover {
422 background: rgba( 12, 11, 15, 0.06 );
423 color: #0c0b0f;
424 }
425 .os-welcome__btn--secondary {
426 color: #2a2533;
427 background: rgba( 242, 82, 252, 0.10 );
428 border-color: rgba( 242, 82, 252, 0.28 );
429 }
430 .os-welcome__btn--secondary:hover {
431 background: rgba( 242, 82, 252, 0.16 );
432 color: #1a1721;
433 }
434 .os-welcome__btn--primary {
435 color: #fffbff;
436 /*
437 * Ends on Pulse, not Nebula. `background-position` slides this
438 * on hover, so every stop carries the label at some point, and
439 * Nebula is light enough that Starlight text on it falls to
440 * ~1.6:1. White-on-Pulse is what <os-button variant="primary">
441 * already uses.
442 */
443 background: linear-gradient( 135deg, #2a2533, #a12bb0 45%, #f252fc );
444 background-size: 180% 180%;
445 box-shadow: 0 10px 24px -10px rgba( 242, 82, 252, 0.75 ),
446 0 4px 10px -4px rgba( 236, 155, 255, 0.55 );
447 }
448 .os-welcome__btn--primary:hover {
449 transform: translateY( -1px );
450 background-position: 100% 50%;
451 box-shadow: 0 14px 30px -12px rgba( 242, 82, 252, 0.85 ),
452 0 6px 14px -4px rgba( 236, 155, 255, 0.65 );
453 }
454 .os-welcome__btn--primary:active {
455 transform: translateY( 0 );
456 }
457 .os-welcome__btn[disabled] {
458 opacity: 0.65;
459 cursor: progress;
460 }
461 body.os-welcome-open {
462 overflow: hidden;
463 }
464 @media ( max-width: 760px ) {
465 .os-welcome__features {
466 grid-template-columns: 1fr;
467 }
468 }
469 @media ( max-width: 600px ) {
470 .os-welcome__hero {
471 padding: 28px 24px 20px;
472 }
473 .os-welcome__body,
474 .os-welcome__actions {
475 padding-left: 24px;
476 padding-right: 24px;
477 }
478 .os-welcome__title {
479 font-size: 22px;
480 }
481 .os-welcome__actions {
482 flex-direction: column-reverse;
483 align-items: stretch;
484 }
485 .os-welcome__btn {
486 width: 100%;
487 }
488 }
489 @media ( prefers-reduced-motion: reduce ) {
490 .os-welcome,
491 .os-welcome__card,
492 .os-welcome__hero,
493 .os-welcome__hint-icon {
494 animation: none !important;
495 }
496 }
497 </style>
498 <div
499 class="os-welcome"
500 role="dialog"
501 aria-modal="true"
502 aria-labelledby="os-welcome-title"
503 aria-describedby="os-welcome-desc"
504 data-slug="<?php echo esc_attr( $slug ); ?>"
505 >
506 <div class="os-welcome__card">
507 <div class="os-welcome__hero">
508 <span class="os-welcome__eyebrow">
509 <span class="os-welcome__eyebrow-dot" aria-hidden="true"></span>
510 <?php echo esc_html__( 'New here', 'desktop-mode' ); ?>
511 </span>
512 <h2 id="os-welcome-title" class="os-welcome__title">
513 <?php echo esc_html( $title ); ?>
514 </h2>
515 <p class="os-welcome__subtitle">
516 <?php echo esc_html( $subtitle ); ?>
517 </p>
518 </div>
519 <div class="os-welcome__body">
520 <p id="os-welcome-desc" class="os-welcome__lede">
521 <?php echo esc_html( $body ); ?>
522 </p>
523 <ul class="os-welcome__features">
524 <?php foreach ( $features as $feature ) : ?>
525 <li class="os-welcome__feature">
526 <span class="os-welcome__feature-icon" aria-hidden="true">
527 <?php echo esc_html( $feature['icon'] ); ?>
528 </span>
529 <strong class="os-welcome__feature-title">
530 <?php echo esc_html( $feature['title'] ); ?>
531 </strong>
532 <p class="os-welcome__feature-desc">
533 <?php echo esc_html( $feature['desc'] ); ?>
534 </p>
535 </li>
536 <?php endforeach; ?>
537 </ul>
538 <p class="os-welcome__hint">
539 <span class="os-welcome__hint-icon" aria-hidden="true"></span>
540 <?php
541 printf(
542 /* translators: %s: the label of the admin bar button. */
543 esc_html__( 'Prefer to switch yourself? Click %s in the top-right of your admin bar.', 'desktop-mode' ),
544 '<code>' . esc_html__( 'Switch to OpenStation', 'desktop-mode' ) . '</code>'
545 );
546 ?>
547 </p>
548 </div>
549 <div class="os-welcome__actions">
550 <button
551 type="button"
552 class="os-welcome__btn os-welcome__btn--secondary"
553 data-os-welcome-cta
554 >
555 <?php echo esc_html( $cta ); ?>
556 </button>
557 <button
558 type="button"
559 class="os-welcome__btn os-welcome__btn--primary"
560 data-os-welcome-enable
561 data-label-idle="<?php echo esc_attr( $enable ); ?>"
562 data-label-busy="<?php echo esc_attr( $enabling ); ?>"
563 >
564 <?php echo esc_html( $enable ); ?>
565 </button>
566 </div>
567 </div>
568 </div>
569 <script id="os-welcome-script">
570 ( function () {
571 var root = document.querySelector( '.os-welcome' );
572 if ( ! root ) {
573 return;
574 }
575 var cfg = {
576 url: <?php echo wp_json_encode( $rest_url ); ?>,
577 nonce: <?php echo wp_json_encode( $rest_nonce ); ?>,
578 slug: <?php echo wp_json_encode( $slug ); ?>,
579 ajaxUrl: <?php echo wp_json_encode( $ajax_url ); ?>,
580 ajaxNonce: <?php echo wp_json_encode( $ajax_nonce ); ?>,
581 };
582
583 document.body.classList.add( 'os-welcome-open' );
584
585 // Focus the primary CTA so keyboard users land somewhere meaningful.
586 var primary = root.querySelector( '[data-os-welcome-enable]' )
587 || root.querySelector( '[data-os-welcome-cta]' );
588 if ( primary ) {
589 try { primary.focus( { preventScroll: true } ); } catch ( e ) {}
590 }
591
592 function close() {
593 if ( ! root || ! root.parentNode ) {
594 return;
595 }
596 root.parentNode.removeChild( root );
597 document.body.classList.remove( 'os-welcome-open' );
598 document.removeEventListener( 'keydown', onKey );
599 }
600
601 // Rebuilds an absolute URL onto the origin the admin page was actually
602 // loaded from. `rest_url()` / `admin_url()` are pinned to `site_url()`,
603 // but the admin may be viewed through a different origin — a reverse
604 // proxy, a Flexible-SSL edge, a mapped multisite domain, or simply an
605 // HTTPS dev proxy in front of an HTTP site. POSTing the *absolute*
606 // site_url URL from such a page is cross-origin (and mixed-content when
607 // the page is HTTPS and site_url is HTTP); the browser blocks it, the
608 // dismissal never reaches the server, and the dialog re-renders on every
609 // page load. Reissuing the request to `window.location.origin` keeps it
610 // same-origin — where the logged-in cookie (domain-scoped, not
611 // port-scoped) and the `wp_rest` nonce (session-bound, origin-agnostic)
612 // are both valid.
613 function sameOrigin( url ) {
614 try {
615 var parsed = new URL( url, window.location.href );
616 return window.location.origin + parsed.pathname + parsed.search;
617 } catch ( e ) {
618 return url;
619 }
620 }
621
622 function persist() {
623 // Fire-and-forget. The seen-intros endpoint always returns the
624 // post-mutation list, but we don't need it here; if the request
625 // fails (offline, REST disabled) the dialog will simply show
626 // again next page load — exactly the behavior a user would
627 // expect from a "save my dismissal" call that didn't reach the
628 // server.
629 var url = sameOrigin( cfg.url );
630 var payload = JSON.stringify( { slug: cfg.slug } );
631
632 // Prefer `navigator.sendBeacon`: it is queued by the browser and
633 // survives the navigation that "Enable it now" triggers without the
634 // keepalive caveats, and it is inherently same-origin-credentialed.
635 // The `wp_rest` nonce rides along as `_wpnonce` (REST cookie auth
636 // reads it from `$_REQUEST`), and the Blob's `application/json` type
637 // lets the REST server parse the `slug` body param.
638 try {
639 if ( navigator.sendBeacon ) {
640 var beaconUrl = url +
641 ( url.indexOf( '?' ) === -1 ? '?' : '&' ) +
642 '_wpnonce=' + encodeURIComponent( cfg.nonce );
643 var blob = new Blob( [ payload ], { type: 'application/json' } );
644 if ( navigator.sendBeacon( beaconUrl, blob ) ) {
645 return;
646 }
647 }
648 } catch ( e ) {}
649
650 // Fallback: `keepalive: true` keeps the POST alive across the
651 // "Enable it now" redirect on browsers without sendBeacon.
652 try {
653 var headers = { 'Content-Type': 'application/json' };
654 if ( cfg.nonce ) {
655 headers[ 'X-WP-Nonce' ] = cfg.nonce;
656 }
657 fetch( url, {
658 method: 'POST',
659 credentials: 'same-origin',
660 keepalive: true,
661 headers: headers,
662 body: payload,
663 } ).catch( function () {} );
664 } catch ( e ) {}
665 }
666
667 function dismiss() {
668 persist();
669 close();
670 }
671
672 var enabling = false;
673
674 function enableNow( btn ) {
675 if ( enabling ) {
676 return;
677 }
678 enabling = true;
679 var idle = btn.getAttribute( 'data-label-idle' ) || btn.textContent;
680 var busy = btn.getAttribute( 'data-label-busy' ) || idle;
681 btn.disabled = true;
682 btn.textContent = busy;
683
684 // Persist the dismissal in parallel — even if the AJAX save fails
685 // the user explicitly asked to turn the mode on, so they don't
686 // need to see the welcome again.
687 persist();
688
689 var form = new FormData();
690 form.append( 'action', 'save-openstation' );
691 form.append( 'nonce', cfg.ajaxNonce );
692 form.append( 'enabled', '1' );
693
694 fetch( sameOrigin( cfg.ajaxUrl ), {
695 method: 'POST',
696 credentials: 'same-origin',
697 body: form,
698 } ).then( function ( r ) {
699 return r.json().catch( function () { return null; } );
700 } ).then( function ( data ) {
701 var redirect = data && data.success && data.data && data.data.redirect
702 ? data.data.redirect
703 : null;
704 if ( redirect ) {
705 window.location.href = redirect;
706 return;
707 }
708 // Fallback — reload so the shell takes over (the AJAX endpoint
709 // already wrote the user meta, so this request will boot
710 // straight into OpenStation via the portal flow).
711 window.location.reload();
712 } ).catch( function () {
713 enabling = false;
714 btn.disabled = false;
715 btn.textContent = idle;
716 } );
717 }
718
719 root.addEventListener( 'click', function ( event ) {
720 var target = event.target;
721 if ( ! ( target instanceof Element ) ) {
722 return;
723 }
724 var enableBtn = target.closest( '[data-os-welcome-enable]' );
725 if ( enableBtn ) {
726 event.preventDefault();
727 enableNow( enableBtn );
728 return;
729 }
730 if ( target.closest( '[data-os-welcome-cta]' ) ) {
731 event.preventDefault();
732 dismiss();
733 return;
734 }
735 // Backdrop click — outside the card.
736 if ( target === root ) {
737 event.preventDefault();
738 dismiss();
739 }
740 } );
741
742 function onKey( event ) {
743 if ( event.key === 'Escape' || event.key === 'Esc' ) {
744 event.preventDefault();
745 dismiss();
746 }
747 }
748 document.addEventListener( 'keydown', onKey );
749 } )();
750 </script>
751 <?php
752 }
753 add_action( 'admin_footer', 'openstation_render_welcome_dialog' );
754