| @@ -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,21 +17,20 @@ | ||
| 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 | - | |
| 27 | - add_action('admin_head', function () { | |
| 28 | - if (!isset($_REQUEST['page']) || $_REQUEST['page'] != 'fluent-booking') { // phpcs:ignore WordPress.Security.NonceVerification.Recommended | |
| 29 | - return; | |
| 30 | - } | |
| 31 | - $this->printThemeClass(); | |
| 32 | - }, 1); | |
| 33 | 33 | } |
| 34 | 34 | |
| 35 | 35 | /** |
| 36 | 36 | * Put the dark class on the document before the page paints. |
| @@ -122,10 +122,90 @@ | ||
| 122 | 122 | $capability, |
| 123 | 123 | 'fluent-booking#/settings/general-settings', |
| 124 | 124 | [$this, 'render'] |
| 125 | 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 | + } | |
| 126 | 139 | } |
| 127 | 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 | + | |
| 128 | 208 | public function render() |
| 129 | 209 | { |
| 130 | 210 | if (!as_has_scheduled_action('fluent_booking_five_minutes_tasks')) { |
| 131 | 211 | as_schedule_recurring_action(time(), (60 * 5), 'fluent_booking_five_minutes_tasks', [], 'fluent-booking', true); |
| @@ -130,8 +210,15 @@ | ||
| 130 | 210 | if (!as_has_scheduled_action('fluent_booking_five_minutes_tasks')) { |
| 131 | 211 | as_schedule_recurring_action(time(), (60 * 5), 'fluent_booking_five_minutes_tasks', [], 'fluent-booking', true); |
| 132 | 212 | } |
| 133 | 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 | + | |
| 134 | 221 | $this->changeFooter(); |
| 135 | 222 | |
| 136 | 223 | $app = App::getInstance(); |
| 137 | 224 | |
| @@ -185,15 +272,16 @@ | ||
| 185 | 272 | |
| 186 | 273 | $assets = $app['url.assets']; |
| 187 | 274 | |
| 188 | 275 | $portalVars = apply_filters('fluent_booking/admin_portal_vars', [ |
| 189 | - 'name' => $name, | |
| 190 | - 'slug' => $slug, | |
| 191 | - 'menuItems' => $menuItems, | |
| 192 | - 'settings' => $settingItems, | |
| 193 | - 'baseUrl' => $baseUrl, | |
| 194 | - 'logo' => $assets . 'images/logo.svg', | |
| 195 | - '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') : '', | |
| 196 | 284 | ]); |
| 197 | 285 | |
| 198 | 286 | do_action('fluent_booking/admin_app_rendering'); |
| 199 | 287 | |
| @@ -212,9 +300,9 @@ | ||
| 212 | 300 | return FLUENT_BOOKING_VERSION; |
| 213 | 301 | }); |
| 214 | 302 | } |
| 215 | 303 | |
| 216 | - public function enqueueAssets() | |
| 304 | + public function enqueueAssets($withApp = true) | |
| 217 | 305 | { |
| 218 | 306 | $app = App::getInstance(); |
| 219 | 307 | |
| 220 | 308 | $isRtl = Helper::fluentbooking_is_rtl(); |
| @@ -318,8 +406,14 @@ | ||
| 318 | 406 | }, 999999); |
| 319 | 407 | |
| 320 | 408 | Vite::enqueueStyle('fluent_booing_admin_app', 'admin_css', [], $assetsVersion, 'all'); |
| 321 | 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 | + | |
| 322 | 416 | do_action($slug . '_loading_app'); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.DynamicHooknameFound |
| 323 | 417 | |
| 324 | 418 | Vite::enqueueScript($slug . '_admin_app', 'admin_app', array('jquery'), $assetsVersion); |
| 325 | 419 | |
| @@ -380,9 +474,8 @@ | ||
| 380 | 474 | $weekSelectTimes = Helper::getWeekSelectTimes(); |
| 381 | 475 | $overrideSelectTimes = Helper::getOverrideSelectTimes(); |
| 382 | 476 | $statusChangingTimes = Helper::getBookingStatusChangingTimes(); |
| 383 | 477 | $defaultTermsAndConditions = Helper::getDefaultTermsAndConditions(); |
| 384 | - $locationFields = (new CalendarSlot())->getLocationFields(); | |
| 385 | 478 | |
| 386 | 479 | return apply_filters('fluent_booking/admin_vars', [ |
| 387 | 480 | 'slug' => $slug = $app->config->get('app.slug'), |
| 388 | 481 | 'nonce' => wp_create_nonce($slug), |
| @@ -394,9 +487,8 @@ | ||
| 394 | 487 | 'multi_durations' => $multiDurations, |
| 395 | 488 | 'buffer_times' => $bufferTimes, |
| 396 | 489 | 'slot_intervals' => $slotIntervals, |
| 397 | 490 | 'schedule_schema' => $scheduleSchema, |
| 398 | - 'location_fields' => $locationFields, | |
| 399 | 491 | 'custom_field_types' => $customFieldTypes, |
| 400 | 492 | 'week_select_times' => $weekSelectTimes, |
| 401 | 493 | 'duration_lookup' => $durationLookup, |
| 402 | 494 | 'multi_duration_lookup' => $multiDurationLookup, |
| @@ -428,8 +520,9 @@ | ||
| 428 | 520 | ], |
| 429 | 521 | 'has_pro' => defined('FLUENT_BOOKING_PRO_DIR_FILE'), |
| 430 | 522 | 'require_upgrade' => defined('FLUENT_BOOKING_PRO_DIR_FILE') && !defined('FLUENT_BOOKING_LITE'), |
| 431 | 523 | 'dashboard_notices' => apply_filters('fluent_booking/dashboard_notices', []), |
| 524 | + 'optin' => $this->getOptinVars(), | |
| 432 | 525 | 'payment_methods' => apply_filters('fluent_booking/payment/get_all_methods', []), |
| 433 | 526 | 'trans' => TransStrings::getStrings(), |
| 434 | 527 | 'date_format' => DateTimeHelper::getDateFormatter(true), |
| 435 | 528 | 'time_format' => DateTimeHelper::getTimeFormatter(true), |