is_v2(); } private function is_our_screen() { // phpcs:ignore WordPress.Security.NonceVerification $page = isset($_GET['page']) ? $_GET['page'] : ''; if ($this->pro_owns_dashboard()) { return false; } // Extension pages are ours whether or not Pro is active — they have no // Pro counterpart. Without this the page would render without the // `pp-scope` body class and @plugpress/ui would be completely unstyled. // Iterate the filter (like Pro does) so any future Lite extension is // covered without touching this method. foreach (apply_filters('divitorque/dashboard_extensions', []) as $ext) { if (!empty($ext['slug']) && $ext['slug'] === $page) { return true; } } return $page === $this->menu_slug && !AdminHelper::is_pro_installed(); } /** * Render a standalone extension settings page inside the v2 app. * * Extensions register their own top-level slug rather than * `divitorque&path=…`, because with Pro active that slug routes to Pro's * dashboard, whose bundle has no route for them. Booting the app with an * explicit `initialPath` is what makes the same bundle serve both. * * @param string $path Route to open, e.g. 'share-my-post'. * * @return void */ public function render_extension_page($path) { $this->enqueue_v2($path); echo '
'; } public function add_menu() { if (!current_user_can($this->capability) || AdminHelper::is_pro_installed()) { return; } // SVG icon (base64 encoded) $icon_svg = 'data:image/svg+xml;base64,' . base64_encode(' '); add_menu_page( __('Divi Torque', 'addons-for-divi'), __('Divi Torque', 'addons-for-divi'), $this->capability, $this->menu_slug, [$this, 'render_app'], $icon_svg, 130 ); foreach ($this->v2_submenus() as $item) { if (isset($item['slug'])) { $slug = $item['slug']; $callback = null; } elseif ('' === $item['path']) { $slug = $this->menu_slug; $callback = [$this, 'render_app']; } else { $slug = "{$this->menu_slug}&path={$item['path']}"; $callback = [$this, 'render_app']; } add_submenu_page( $this->menu_slug, $item['label'], $item['label'], $this->capability, $slug, $callback ); } } /** * Dashboard v2 submenu map — one entry per app destination, mirroring * the PageDefs in admin/src/index.js. The WP menu is the app's ONLY * navigation; the app's top bar carries brand, versions and docs. * (Sharing Buttons registers its own slug in share-my-post/admin.php.) * * In legacy mode only the two v1 destinations exist. * * @return array[] */ private function v2_submenus() { $items = [ [ 'label' => __('Overview', 'addons-for-divi'), 'path' => '', ], [ 'label' => __('Modules', 'addons-for-divi'), 'path' => 'module-manager', ], ]; if ($this->is_v2()) { $items[] = [ 'label' => '', 'path' => null, 'slug' => 'divitorque-lite-sep-1', ]; // Sharing Buttons registers its own slug at admin_menu 20, landing // after Extensions — together they form the extensions group. $items[] = [ 'label' => __('Extensions', 'addons-for-divi'), 'path' => 'extensions', ]; } return $items; } /** * Group separators: the dummy `divitorque-lite-sep-N` submenu entries * render as thin unclickable rules. The menu is printed on every admin * screen, so this rides admin_head globally. * * @return void */ public function print_menu_separator_css() { if (!$this->is_v2() || AdminHelper::is_pro_installed()) { return; } echo ''; } /** * Highlight the submenu entry matching the app's `?path=` arg. * * The v2 slugs embed the path ("divitorque&path=extensions") but WP only * auto-highlights the entry whose slug equals $plugin_page ('divitorque'), * so every deep link lit up "Dashboard". Map the request back to the * registered slug; the Sharing Buttons route maps to the extension's own * registered submenu. * * @param string|null $submenu_file Submenu file WP computed. * @param string $parent_file Parent menu file. * * @return string|null */ public function highlight_submenu($submenu_file, $parent_file) { // phpcs:ignore WordPress.Security.NonceVerification $page = isset($_GET['page']) ? $_GET['page'] : ''; if (!$this->is_v2() || $page !== $this->menu_slug || AdminHelper::is_pro_installed()) { return $submenu_file; } $path = isset($_GET['path']) ? sanitize_key(wp_unslash($_GET['path'])) : ''; // phpcs:ignore WordPress.Security.NonceVerification if ('' === $path) { return $submenu_file; } if ('share-my-post' === $path) { return Share_My_Post_Admin::SLUG; } foreach ($this->v2_submenus() as $item) { if ($item['path'] === $path) { return "{$this->menu_slug}&path={$path}"; } } return $submenu_file; } public function render_app() { if ($this->is_v2()) { $this->enqueue_v2(); echo '
'; return; } $this->enqueue_scripts(); echo '
'; } /* ─── Dashboard v2 ─────────────────────────────────────────────── */ private function enqueue_v2($initial_path = '') { $base = DIVI_TORQUE_LITE_URL . 'admin/build/'; $dir = DIVI_TORQUE_LITE_DIR . 'admin/build/'; // Version by file mtime so a rebuilt bundle always busts the browser/page // cache, even within the same plugin version. A static ?ver lets a stale // cached bundle load against a fresh runtime. $css_ver = is_readable($dir . 'index.css') ? (string) filemtime($dir . 'index.css') : DIVI_TORQUE_LITE_VERSION; $js_ver = is_readable($dir . 'index.js') ? (string) filemtime($dir . 'index.js') : DIVI_TORQUE_LITE_VERSION; wp_enqueue_style('divi-torque-lite-admin-v2', $base . 'index.css', [], $css_ver); wp_enqueue_script( 'divi-torque-lite-admin-v2', $base . 'index.js', ['wp-api-fetch', 'wp-i18n'], $js_ver, true ); wp_add_inline_script( 'divi-torque-lite-admin-v2', 'window.divitorqueData = ' . wp_json_encode($this->get_v2_data($initial_path)) . ';', 'before' ); } private function get_v2_data($initial_path = '') { return [ // Set only on extension pages, which have their own WP menu slug and // so cannot use the `?path=` bridge the dashboard tabs use. 'initialPath' => $initial_path, // The app hides the dashboard/module routes when Pro owns them, so // an extension page opened under Pro shows only itself. 'proActive' => AdminHelper::is_pro_installed(), // Extension pages' REST namespace + server data — same shape Pro's // boot data carries, fed by the same filter. 'extensions' => apply_filters('divitorque/dashboard_extensions', []), 'root' => esc_url_raw(get_rest_url()), 'nonce' => wp_create_nonce('wp_rest'), 'ns' => 'divitorque-lite/v1', 'version' => DIVI_TORQUE_LITE_VERSION, 'adminUrl' => esc_url_raw(admin_url()), 'docsUrl' => 'https://divitorque.com/docs/', 'upgradeUrl' => 'https://divitorque.com/pricing/?utm_source=divi-torque-lite&utm_medium=wp-admin&utm_campaign=upgrade-to-pro', 'rollbackUrl' => esc_url_raw(admin_url('admin.php?page=divitorque-rollback')), 'moduleInfo' => ModulesManager::get_all_modules(), // Welcome tour (admin/src/pages/Onboarding.jsx). 'isDivi5' => function_exists('divitorque_lite_d5_enabled') && divitorque_lite_d5_enabled(), 'onboardingDone' => self::onboarding_done(), // Lite's own extensions (the catalog lists only the marketed ones). 'freeExtensions' => class_exists(__NAMESPACE__ . '\Extensions_Manager') ? count(array_filter(Extensions_Manager::all(), function ($ext) { return empty($ext['locked']); })) : 0, 'proDiscountPercent' => (int) self::PRO_DISCOUNT, 'pricingUrl' => 'https://divitorque.com/pricing/?utm_source=divi-torque-lite&utm_medium=wp-admin&utm_campaign=onboarding', 'saddleUrl' => 'https://saddle.to/?utm_source=divi-torque-lite&utm_medium=wp-admin&utm_campaign=onboarding', ]; } public function body_class($classes) { if ($this->is_v2() && $this->is_our_screen()) { $classes .= ' pp-scope'; } return $classes; } /* ─── v1 ⇄ v2 switching ────────────────────────────────────────── */ public function register_switch_route() { register_rest_route('divitorque-lite/v1', '/switch_dashboard_ui', [ 'methods' => 'POST', 'callback' => function ($request) { $ui = $request->get_param('ui') === 'legacy' ? 'legacy' : 'v2'; update_option(self::UI_OPTION, $ui); return rest_ensure_response(['ui' => $ui]); }, 'permission_callback' => function () { return current_user_can($this->capability); }, ]); } /** Nonce link on the legacy banner: admin.php?page=divitorque&dtl_ui=v2 */ public function handle_switch_link() { if (!isset($_GET['dtl_ui']) || !$this->is_our_screen()) { return; } if (!current_user_can($this->capability)) { return; } check_admin_referer('dtl_switch_ui'); update_option(self::UI_OPTION, $_GET['dtl_ui'] === 'legacy' ? 'legacy' : 'v2'); wp_safe_redirect(admin_url('admin.php?page=' . $this->menu_slug)); exit; } /** * Send the admin who just activated Lite to the welcome tour, once. * * The transient is deleted before any other check, so a skipped redirect * (bulk activation, Pro present, tour already done) is never retried on a * later page load. */ public function maybe_redirect_to_onboarding() { if (!get_transient(self::ONBOARDING_TRANSIENT)) { return; } delete_transient(self::ONBOARDING_TRANSIENT); if (!self::should_redirect_to_onboarding()) { return; } wp_safe_redirect(admin_url('admin.php?page=' . $this->menu_slug . '&path=welcome')); exit; } /** * Whether the current request may be taken over by the onboarding redirect. * * @return bool */ public static function should_redirect_to_onboarding() { // phpcs:ignore WordPress.Security.NonceVerification if (isset($_GET['activate-multi'])) { return false; } if (wp_doing_ajax() || wp_doing_cron() || (defined('WP_CLI') && WP_CLI) || is_network_admin()) { return false; } if (!current_user_can('manage_options') || AdminHelper::is_pro_installed()) { return false; } if (get_option(self::UI_OPTION, 'v2') === 'legacy') { return false; } return !self::onboarding_done(); } /** * Whether the current user finished or skipped the welcome tour. * * @return bool */ public static function onboarding_done() { return (bool) get_user_meta(get_current_user_id(), self::ONBOARDING_META, true); } /* ─── Legacy v1 enqueue — unchanged ────────────────────────────── */ public function enqueue_scripts() { $assets_url = DIVI_TORQUE_LITE_URL . 'assets'; $dashboard_js = $assets_url . '/admin/js/dashboard.js'; $dashboard_css = $assets_url . '/admin/css/dashboard.css'; wp_enqueue_script( 'divi-torque-lite-dashboard', $dashboard_js, $this->wp_deps(), DIVI_TORQUE_LITE_VERSION, true ); wp_enqueue_style( 'divi-torque-lite-dashboard', $dashboard_css, ['wp-components'], DIVI_TORQUE_LITE_VERSION ); $localize = [ 'root' => esc_url_raw(get_rest_url()), 'admin_slug' => $this->menu_slug, 'nonce' => wp_create_nonce('wp_rest'), 'assetsPath' => esc_url_raw($assets_url), 'version' => DIVI_TORQUE_LITE_VERSION, 'module_info' => ModulesManager::get_all_modules(), 'module_icon_path' => DIVI_TORQUE_LITE_URL . 'assets/imgs/icons', 'isProInstalled' => AdminHelper::is_pro_installed(), 'upgradeLink' => 'https://divitorque.com/pricing-lifetime/?utm_source=divi-torque-lite&utm_medium=wp-admin&utm_campaign=upgrade-to-pro&utm_content=menu-button', 'currentVersion' => DIVI_TORQUE_LITE_VERSION, ]; wp_localize_script('divi-torque-lite-dashboard', 'diviTorqueLite', $localize); } public function wp_deps() { return [ 'react', 'wp-api', 'wp-i18n', 'lodash', 'wp-components', 'wp-element', 'wp-api-fetch', 'wp-core-data', 'wp-data', 'wp-dom-ready', ]; } }