| @@ -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, |