PluginProbe
Fluent Booking – The Ultimate Appointments Scheduling, Events Booking, Events Calendar Solution / 2.5.0
Fluent Booking – The Ultimate Appointments Scheduling, Events Booking, Events Calendar Solution v2.5.0
2.5.0 2.4.0 2.3.0 2.2.5 2.2.0 2.1.2 2.1.1 trunk 1.10.0 1.10.01 1.10.02 1.5.0 1.5.01 1.5.02 1.5.1 1.5.10 1.5.20 1.5.21 1.5.22 1.5.23 1.5.24 1.5.25 1.6.0 1.7.0 1.7.1 All 34 releases
← All changes | app/Hooks/Handlers/AdminMenuHandler.php +163 -33 2.1.2 → 2.5.0 View file →
@@ -2,14 +2,16 @@
2 2
3 3 namespace FluentBooking\App\Hooks\Handlers;
4 4
5 5 use FluentBooking\App\App;
6 +use FluentBooking\App\Vite;
7 +use FluentBooking\App\Models\Booking;
6 8 use FluentBooking\App\Models\Calendar;
7 -use FluentBooking\App\Models\CalendarSlot;
8 9 use FluentBooking\App\Services\DateTimeHelper;
9 10 use FluentBooking\App\Services\Helper;
10 11 use FluentBooking\App\Services\PermissionManager;
11 12 use FluentBooking\App\Services\TransStrings;
13 +use FluentBooking\Framework\Support\Arr;
12 14
13 15 class AdminMenuHandler
14 16 {
15 17 public function register()
@@ -15,16 +17,50 @@
15 17 public function register()
16 18 {
17 19 add_action('admin_menu', [$this, 'add']);
18 20
19 - add_action('admin_enqueue_scripts', function () {
20 - if (!isset($_REQUEST['page']) || $_REQUEST['page'] != 'fluent-booking') { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
21 + add_action('admin_enqueue_scripts', function ($hookSuffix) {
22 + if ($hookSuffix !== 'toplevel_page_fluent-booking') {
21 23 return;
22 24 }
25 +
23 26 $this->enqueueAssets();
27 +
28 + add_action('admin_head', function () {
29 + $this->printThemeClass();
30 + }, 1);
31 +
24 32 }, 100);
25 33 }
26 34
35 + /**
36 + * Put the dark class on the document before the page paints.
37 + *
38 + * The theme is chosen in the browser, so the server cannot know it - which means
39 + * without this the first frame is always light, and somebody on dark gets a white flash
40 + * on every page load. This runs in <head>, before wp-admin's own markup, and is the
41 + * only reason it is inline rather than in global_admin.js: a file in the footer is too
42 + * late to matter, and one in the header is still a request the paint would wait on.
43 + *
44 + * `fluent_theme_mode` is the key every Fluent plugin shares (see global_admin.js), and it
45 + * holds a bare `light`, `dark` or `system`, so `system` is resolved here against the
46 + * machine's own preference. <body> does not exist yet, so only <html> is marked;
47 + * global_admin.js puts the same classes on <body> and #wpbody-content once it runs.
48 + *
49 + * @return void
50 + */
51 + public function printThemeClass()
52 + {
53 + $script = <<<'JS'
54 +(function(){try{var s=localStorage.getItem('fluent_theme_mode')||'';
55 +if(!s){s=localStorage.getItem('fcal_color_mode')==='dark'?'dark':'light';}
56 +if(s==='system'){s='system:'+(window.matchMedia&&window.matchMedia('(prefers-color-scheme: dark)').matches?'dark':'light');}
57 +if(s==='dark'||s==='system:dark'){var e=document.documentElement;e.classList.add('fluent_theme_dark');e.classList.add('dark');}}catch(e){}})();
58 +JS;
59 +
60 + echo '<script id="fluent-booking-theme-class">' . $script . '</script>'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
61 + }
62 +
27 63 public function add()
28 64 {
29 65 $capability = PermissionManager::getMenuPermission();
30 66 $menuPriority = 26;
@@ -86,10 +122,90 @@
86 122 $capability,
87 123 'fluent-booking#/settings/general-settings',
88 124 [$this, 'render']
89 125 );
126 +
127 + if ($this->shouldShowUpgrade()) {
128 + add_submenu_page(
129 + 'fluent-booking',
130 + __('Upgrade to Pro', 'fluent-booking'),
131 + '<span class="fcal_upgrade_pro_menu">' . esc_html__('Upgrade to Pro', 'fluent-booking') . '</span>',
132 + 'manage_options',
133 + Helper::getUpgradeUrl('admin_sidebar_menu')
134 + );
135 +
136 + add_action('admin_head', [$this, 'printUpgradeMenuStyle']);
137 + add_action('admin_footer', [$this, 'printUpgradeMenuScript']);
138 + }
90 139 }
91 140
141 + /**
142 + * The upsell is for whoever can buy the license, so it stays hidden once Pro is loaded
143 + * and from hosts who only reach the app through their own calendars.
144 + *
145 + * @return bool
146 + */
147 + public function shouldShowUpgrade()
148 + {
149 + return !defined('FLUENT_BOOKING_PRO_DIR_FILE') && current_user_can('manage_options');
150 + }
151 +
152 + /**
153 + * Onboarding asks on its final step. The dashboard card waits until the site has a completed
154 + * booking, when FluentBooking has proven useful, and counts one the way the Bookings page's
155 + * "Completed" tab does.
156 + *
157 + * @return array
158 + */
159 + private function getOptinVars()
160 + {
161 + $showPrompt = $this->showOptinPrompt();
162 +
163 + return [
164 + 'share_stats' => Arr::get((array) Helper::getMeta('option', 0, 'optin'), 'share_stats', ''),
165 + 'can_manage' => PermissionManager::userCan('manage_all_data'),
166 + 'show_prompt' => $showPrompt,
167 + 'show_card' => $showPrompt && Booking::applyComputedStatus('completed')->exists(),
168 + ];
169 + }
170 +
171 + /**
172 + * The email opt-in prompt (dashboard card and onboarding) is for free sites that have not
173 + * subscribed yet. Closing it hides it for 30 days.
174 + *
175 + * @return bool
176 + */
177 + private function showOptinPrompt()
178 + {
179 + if (defined('FLUENT_BOOKING_PRO_DIR_FILE') || !PermissionManager::userCan('manage_all_data')) {
180 + return false;
181 + }
182 +
183 + $optin = (array) Helper::getMeta('option', 0, 'optin');
184 +
185 + if (!empty($optin['email'])) {
186 + return false;
187 + }
188 +
189 + return empty($optin['dismissed_at']) || (time() - (int) $optin['dismissed_at']) > 30 * DAY_IN_SECONDS;
190 + }
191 +
192 + public function printUpgradeMenuStyle()
193 + {
194 + echo '<style id="fluent-booking-upgrade-menu">#adminmenu .fcal_upgrade_pro_menu{color:#72aee6;font-weight:500;}</style>';
195 + }
196 +
197 + /**
198 + * The submenu points off-site, and WordPress gives submenu links no target, so it would
199 + * otherwise take the admin away from the dashboard.
200 + *
201 + * @return void
202 + */
203 + public function printUpgradeMenuScript()
204 + {
205 + echo '<script id="fluent-booking-upgrade-menu-js">(function(){var s=document.querySelector("#adminmenu .fcal_upgrade_pro_menu");if(s&&s.parentNode){s.parentNode.setAttribute("target","_blank");s.parentNode.setAttribute("rel","noopener");}})();</script>';
206 + }
207 +
92 208 public function render()
93 209 {
94 210 if (!as_has_scheduled_action('fluent_booking_five_minutes_tasks')) {
95 211 as_schedule_recurring_action(time(), (60 * 5), 'fluent_booking_five_minutes_tasks', [], 'fluent-booking', true);
@@ -94,8 +210,15 @@
94 210 if (!as_has_scheduled_action('fluent_booking_five_minutes_tasks')) {
95 211 as_schedule_recurring_action(time(), (60 * 5), 'fluent_booking_five_minutes_tasks', [], 'fluent-booking', true);
96 212 }
97 213
214 + // The daily task carries the summary report and the MCP record purge.
215 + // A site that never got it — a secondary blog on a network activation,
216 + // or one that lost it — would grow fcal_mcp_g_* rows forever.
217 + if (!as_has_scheduled_action('fluent_booking/daily_tasks')) {
218 + as_schedule_recurring_action(time(), (60 * 60 * 24), 'fluent_booking/daily_tasks', [], 'fluent-booking', true);
219 + }
220 +
98 221 $this->changeFooter();
99 222
100 223 $app = App::getInstance();
101 224
@@ -149,15 +272,16 @@
149 272
150 273 $assets = $app['url.assets'];
151 274
152 275 $portalVars = apply_filters('fluent_booking/admin_portal_vars', [
153 - 'name' => $name,
154 - 'slug' => $slug,
155 - 'menuItems' => $menuItems,
156 - 'settings' => $settingItems,
157 - 'baseUrl' => $baseUrl,
158 - 'logo' => $assets . 'images/logo.svg',
159 - 'dark_logo' => $assets . 'images/logo_dark.svg',
276 + 'name' => $name,
277 + 'slug' => $slug,
278 + 'menuItems' => $menuItems,
279 + 'settings' => $settingItems,
280 + 'baseUrl' => $baseUrl,
281 + 'logo' => $assets . 'images/logo.svg',
282 + 'dark_logo' => $assets . 'images/logo_dark.svg',
283 + 'upgradeUrl' => $this->shouldShowUpgrade() ? Helper::getUpgradeUrl('top_bar') : '',
160 284 ]);
161 285
162 286 do_action('fluent_booking/admin_app_rendering');
163 287
@@ -176,22 +300,20 @@
176 300 return FLUENT_BOOKING_VERSION;
177 301 });
178 302 }
179 303
180 - public function enqueueAssets()
304 + public function enqueueAssets($withApp = true)
181 305 {
182 306 $app = App::getInstance();
183 307
184 308 $isRtl = Helper::fluentbooking_is_rtl();
185 309
186 - $assets = $app['url.assets'];
310 + $slug = $app->config->get('app.slug');
187 311
188 - $slug = $app->config->get('app.slug');
312 + $assetsVersion = Vite::isDev() ? time() : FLUENT_BOOKING_ASSETS_VERSION;
189 313
190 - $adminAppCss = 'admin/admin.css';
191 314 if ($isRtl) {
192 - $adminAppCss = 'admin/admin-rtl.css';
193 - wp_enqueue_style('fluentbooking_admin_rtl', $assets . 'admin/fluentbooking_admin_rtl.css', [], FLUENT_BOOKING_ASSETS_VERSION);
315 + Vite::enqueueStyle('fluentbooking_admin_rtl', 'admin_rtl_css', [], $assetsVersion);
194 316 }
195 317
196 318 add_action('wp_print_scripts', function () {
197 319
@@ -282,27 +404,21 @@
282 404 wp_dequeue_style($wp_styles->registered[$script]->handle);
283 405 }
284 406 }, 999999);
285 407
286 - wp_enqueue_style('fluent_booing_admin_app', $assets . $adminAppCss, [], FLUENT_BOOKING_ASSETS_VERSION, 'all');
408 + Vite::enqueueStyle('fluent_booing_admin_app', 'admin_css', [], $assetsVersion, 'all');
287 409
410 + // The dashboard payload is per-user and includes every host's email address,
411 + // so the app bundle is only ever emitted for an authenticated, permitted viewer.
412 + if (!$withApp || !get_current_user_id()) {
413 + return;
414 + }
415 +
288 416 do_action($slug . '_loading_app'); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.DynamicHooknameFound
289 417
290 - wp_enqueue_script(
291 - $slug . '_admin_app',
292 - $assets . 'admin/app.js',
293 - array('jquery'),
294 - FLUENT_BOOKING_ASSETS_VERSION,
295 - true
296 - );
418 + Vite::enqueueScript($slug . '_admin_app', 'admin_app', array('jquery'), $assetsVersion);
297 419
298 - wp_enqueue_script(
299 - $slug . '_global_admin',
300 - $assets . 'admin/global_admin.js',
301 - array(),
302 - FLUENT_BOOKING_ASSETS_VERSION,
303 - true
304 - );
420 + Vite::enqueueScript($slug . '_global_admin', 'global_admin', array(), $assetsVersion);
305 421
306 422 if (function_exists('wp_enqueue_editor')) {
307 423 add_filter('user_can_richedit', '__return_true');
308 424 wp_enqueue_editor();
@@ -358,9 +474,8 @@
358 474 $weekSelectTimes = Helper::getWeekSelectTimes();
359 475 $overrideSelectTimes = Helper::getOverrideSelectTimes();
360 476 $statusChangingTimes = Helper::getBookingStatusChangingTimes();
361 477 $defaultTermsAndConditions = Helper::getDefaultTermsAndConditions();
362 - $locationFields = (new CalendarSlot())->getLocationFields();
363 478
364 479 return apply_filters('fluent_booking/admin_vars', [
365 480 'slug' => $slug = $app->config->get('app.slug'),
366 481 'nonce' => wp_create_nonce($slug),
@@ -372,9 +487,8 @@
372 487 'multi_durations' => $multiDurations,
373 488 'buffer_times' => $bufferTimes,
374 489 'slot_intervals' => $slotIntervals,
375 490 'schedule_schema' => $scheduleSchema,
376 - 'location_fields' => $locationFields,
377 491 'custom_field_types' => $customFieldTypes,
378 492 'week_select_times' => $weekSelectTimes,
379 493 'duration_lookup' => $durationLookup,
380 494 'multi_duration_lookup' => $multiDurationLookup,
@@ -384,8 +498,9 @@
384 498 'me' => [
385 499 'id' => $currentUser->ID,
386 500 'calendar_id' => $calendarId,
387 501 'full_name' => $currentUsername,
502 + 'avatar' => get_avatar_url($currentUser->ID, ['size' => 96]),
388 503 'email' => $currentUser->user_email,
389 504 'is_admin' => $hasAllAccess,
390 505 'permissions' => PermissionManager::getUserPermissions($currentUser, false),
391 506 ],
@@ -392,8 +507,9 @@
392 507 'all_hosts' => Calendar::getAllHosts(),
393 508 'is_new' => $isNew,
394 509 'require_slug' => $requireSlug,
395 510 'site_url' => site_url('/'),
511 + 'site_title' => get_bloginfo('name'),
396 512 'upgrade_url' => Helper::getUpgradeUrl(),
397 513 'timezones' => DateTimeHelper::getTimeZones(true),
398 514 'features' => Helper::getFeatures(),
399 515 'supported_features' => apply_filters('fluent_booking/supported_featured', [
@@ -404,8 +520,9 @@
404 520 ],
405 521 'has_pro' => defined('FLUENT_BOOKING_PRO_DIR_FILE'),
406 522 'require_upgrade' => defined('FLUENT_BOOKING_PRO_DIR_FILE') && !defined('FLUENT_BOOKING_LITE'),
407 523 'dashboard_notices' => apply_filters('fluent_booking/dashboard_notices', []),
524 + 'optin' => $this->getOptinVars(),
408 525 'payment_methods' => apply_filters('fluent_booking/payment/get_all_methods', []),
409 526 'trans' => TransStrings::getStrings(),
410 527 'date_format' => DateTimeHelper::getDateFormatter(true),
411 528 'time_format' => DateTimeHelper::getTimeFormatter(true),
@@ -816,8 +933,21 @@
816 933 ]
817 934 ],
818 935 'label' => __('Calendar Settings', 'fluent-booking'),
819 936 'svgIcon' => '<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M12 15C13.6569 15 15 13.6569 15 12C15 10.3431 13.6569 9 12 9C10.3431 9 9 10.3431 9 12C9 13.6569 10.3431 15 12 15Z" stroke="currentColor" stroke-width="1.5" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/><path d="M2 12.8799V11.1199C2 10.0799 2.85 9.21994 3.9 9.21994C5.71 9.21994 6.45 7.93994 5.54 6.36994C5.02 5.46994 5.33 4.29994 6.24 3.77994L7.97 2.78994C8.76 2.31994 9.78 2.59994 10.25 3.38994L10.36 3.57994C11.26 5.14994 12.74 5.14994 13.65 3.57994L13.76 3.38994C14.23 2.59994 15.25 2.31994 16.04 2.78994L17.77 3.77994C18.68 4.29994 18.99 5.46994 18.47 6.36994C17.56 7.93994 18.3 9.21994 20.11 9.21994C21.15 9.21994 22.01 10.0699 22.01 11.1199V12.8799C22.01 13.9199 21.16 14.7799 20.11 14.7799C18.3 14.7799 17.56 16.0599 18.47 17.6299C18.99 18.5399 18.68 19.6999 17.77 20.2199L16.04 21.2099C15.25 21.6799 14.23 21.3999 13.76 20.6099L13.65 20.4199C12.75 18.8499 11.27 18.8499 10.36 20.4199L10.25 20.6099C9.78 21.3999 8.76 21.6799 7.97 21.2099L6.24 20.2199C5.33 19.6999 5.02 18.5299 5.54 17.6299C6.45 16.0599 5.71 14.7799 3.9 14.7799C2.85 14.7799 2 13.9199 2 12.8799Z" stroke="currentColor" stroke-width="1.5" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/></svg>'
937 + ],
938 + 'calendar_events' => [
939 + 'type' => 'route',
940 + 'visible' => true,
941 + 'disable' => false,
942 + 'route' => [
943 + 'name' => 'calendar_events',
944 + 'params' => [
945 + 'calendar_id' => $calendar->id
946 + ]
947 + ],
948 + 'label' => __('Event Types', 'fluent-booking'),
949 + 'svgIcon' => '<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M8 6H21" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/><path d="M8 12H21" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/><path d="M8 18H21" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/><path d="M3 6H3.01" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/><path d="M3 12H3.01" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/><path d="M3 18H3.01" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>'
820 950 ],
821 951 'remote_calendars' => [
822 952 'type' => 'route',
823 953 'visible' => true,