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 +143 -13 2.2.5 → 2.5.0 View file →
@@ -3,14 +3,15 @@
3 3 namespace FluentBooking\App\Hooks\Handlers;
4 4
5 5 use FluentBooking\App\App;
6 6 use FluentBooking\App\Vite;
7 +use FluentBooking\App\Models\Booking;
7 8 use FluentBooking\App\Models\Calendar;
8 -use FluentBooking\App\Models\CalendarSlot;
9 9 use FluentBooking\App\Services\DateTimeHelper;
10 10 use FluentBooking\App\Services\Helper;
11 11 use FluentBooking\App\Services\PermissionManager;
12 12 use FluentBooking\App\Services\TransStrings;
13 +use FluentBooking\Framework\Support\Arr;
13 14
14 15 class AdminMenuHandler
15 16 {
16 17 public function register()
@@ -16,16 +17,50 @@
16 17 public function register()
17 18 {
18 19 add_action('admin_menu', [$this, 'add']);
19 20
20 - add_action('admin_enqueue_scripts', function () {
21 - 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') {
22 23 return;
23 24 }
25 +
24 26 $this->enqueueAssets();
27 +
28 + add_action('admin_head', function () {
29 + $this->printThemeClass();
30 + }, 1);
31 +
25 32 }, 100);
26 33 }
27 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 +
28 63 public function add()
29 64 {
30 65 $capability = PermissionManager::getMenuPermission();
31 66 $menuPriority = 26;
@@ -87,10 +122,90 @@
87 122 $capability,
88 123 'fluent-booking#/settings/general-settings',
89 124 [$this, 'render']
90 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 + }
91 139 }
92 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 +
93 208 public function render()
94 209 {
95 210 if (!as_has_scheduled_action('fluent_booking_five_minutes_tasks')) {
96 211 as_schedule_recurring_action(time(), (60 * 5), 'fluent_booking_five_minutes_tasks', [], 'fluent-booking', true);
@@ -95,8 +210,15 @@
95 210 if (!as_has_scheduled_action('fluent_booking_five_minutes_tasks')) {
96 211 as_schedule_recurring_action(time(), (60 * 5), 'fluent_booking_five_minutes_tasks', [], 'fluent-booking', true);
97 212 }
98 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 +
99 221 $this->changeFooter();
100 222
101 223 $app = App::getInstance();
102 224
@@ -150,15 +272,16 @@
150 272
151 273 $assets = $app['url.assets'];
152 274
153 275 $portalVars = apply_filters('fluent_booking/admin_portal_vars', [
154 - 'name' => $name,
155 - 'slug' => $slug,
156 - 'menuItems' => $menuItems,
157 - 'settings' => $settingItems,
158 - 'baseUrl' => $baseUrl,
159 - 'logo' => $assets . 'images/logo.svg',
160 - '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') : '',
161 284 ]);
162 285
163 286 do_action('fluent_booking/admin_app_rendering');
164 287
@@ -177,9 +300,9 @@
177 300 return FLUENT_BOOKING_VERSION;
178 301 });
179 302 }
180 303
181 - public function enqueueAssets()
304 + public function enqueueAssets($withApp = true)
182 305 {
183 306 $app = App::getInstance();
184 307
185 308 $isRtl = Helper::fluentbooking_is_rtl();
@@ -283,8 +406,14 @@
283 406 }, 999999);
284 407
285 408 Vite::enqueueStyle('fluent_booing_admin_app', 'admin_css', [], $assetsVersion, 'all');
286 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 +
287 416 do_action($slug . '_loading_app'); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.DynamicHooknameFound
288 417
289 418 Vite::enqueueScript($slug . '_admin_app', 'admin_app', array('jquery'), $assetsVersion);
290 419
@@ -345,9 +474,8 @@
345 474 $weekSelectTimes = Helper::getWeekSelectTimes();
346 475 $overrideSelectTimes = Helper::getOverrideSelectTimes();
347 476 $statusChangingTimes = Helper::getBookingStatusChangingTimes();
348 477 $defaultTermsAndConditions = Helper::getDefaultTermsAndConditions();
349 - $locationFields = (new CalendarSlot())->getLocationFields();
350 478
351 479 return apply_filters('fluent_booking/admin_vars', [
352 480 'slug' => $slug = $app->config->get('app.slug'),
353 481 'nonce' => wp_create_nonce($slug),
@@ -359,9 +487,8 @@
359 487 'multi_durations' => $multiDurations,
360 488 'buffer_times' => $bufferTimes,
361 489 'slot_intervals' => $slotIntervals,
362 490 'schedule_schema' => $scheduleSchema,
363 - 'location_fields' => $locationFields,
364 491 'custom_field_types' => $customFieldTypes,
365 492 'week_select_times' => $weekSelectTimes,
366 493 'duration_lookup' => $durationLookup,
367 494 'multi_duration_lookup' => $multiDurationLookup,
@@ -371,8 +498,9 @@
371 498 'me' => [
372 499 'id' => $currentUser->ID,
373 500 'calendar_id' => $calendarId,
374 501 'full_name' => $currentUsername,
502 + 'avatar' => get_avatar_url($currentUser->ID, ['size' => 96]),
375 503 'email' => $currentUser->user_email,
376 504 'is_admin' => $hasAllAccess,
377 505 'permissions' => PermissionManager::getUserPermissions($currentUser, false),
378 506 ],
@@ -379,8 +507,9 @@
379 507 'all_hosts' => Calendar::getAllHosts(),
380 508 'is_new' => $isNew,
381 509 'require_slug' => $requireSlug,
382 510 'site_url' => site_url('/'),
511 + 'site_title' => get_bloginfo('name'),
383 512 'upgrade_url' => Helper::getUpgradeUrl(),
384 513 'timezones' => DateTimeHelper::getTimeZones(true),
385 514 'features' => Helper::getFeatures(),
386 515 'supported_features' => apply_filters('fluent_booking/supported_featured', [
@@ -391,8 +520,9 @@
391 520 ],
392 521 'has_pro' => defined('FLUENT_BOOKING_PRO_DIR_FILE'),
393 522 'require_upgrade' => defined('FLUENT_BOOKING_PRO_DIR_FILE') && !defined('FLUENT_BOOKING_LITE'),
394 523 'dashboard_notices' => apply_filters('fluent_booking/dashboard_notices', []),
524 + 'optin' => $this->getOptinVars(),
395 525 'payment_methods' => apply_filters('fluent_booking/payment/get_all_methods', []),
396 526 'trans' => TransStrings::getStrings(),
397 527 'date_format' => DateTimeHelper::getDateFormatter(true),
398 528 'time_format' => DateTimeHelper::getTimeFormatter(true),