add_filter('pricing_url', [$this, 'customizePricingUrl']); \DaftPlug\Rankify\rankify_pro()->add_filter('plugin_icon', [$this, 'pluginIcon']); \DaftPlug\Rankify\rankify_pro()->add_filter('is_submenu_visible', [$this, 'showContactOrSupportForum'], 10, 2); if (Utils::hasActivePro()) { \DaftPlug\Rankify\rankify_pro()->add_filter('show_deactivation_subscription_cancellation', '__return_false'); } if (\DaftPlug\Rankify\rankify_pro()->is_premium()) { \DaftPlug\Rankify\rankify_pro()->add_filter('connect/before', [$this, 'customizeActivationScreen']); } } /** * Local file Freemius uses for pricing, account and the connect header. * A path, not a URL: the SDK turns it into an image URL itself. */ public function pluginIcon() { return RANKIFY_DIR_PATH . 'assets/media/icons/icon-256x256.png'; } public function customizeActivationScreen($activation_state) { $pluginIconUrl = esc_url(plugins_url('assets/media/icons/icon-256x256.png', RANKIFY_FILE)); echo ' '; if ($activation_state['is_license_activation']) { echo ' '; } } public function customizePricingUrl($url) { return Utils::getUpgradeUrl(); } public function showContactOrSupportForum($is_visible, $menu_id) { if ($menu_id === 'contact') { return \DaftPlug\Rankify\rankify_pro()->can_use_premium_code(); } if ($menu_id === 'support') { return !\DaftPlug\Rankify\rankify_pro()->can_use_premium_code(); } return $is_visible; } public function addPluginActionLinks($links) { if (!Utils::hasActivePro()) { $links['upgrade'] = '' . Utils::getUpgradeLabel() . ''; } $links['settings'] = '' . __('Settings', 'crownify') . ''; return $links; } /** * Sidebar glyph. svg-painter recolours a base64 SVG whose shapes are * fill="black"; a normal file URL would stay black in every color scheme. */ private function menuIcon() { $path = RANKIFY_DIR_PATH . 'assets/media/icons/crownify-menu-icon.svg'; $svg = is_readable($path) ? file_get_contents($path) : ''; if (!is_string($svg) || '' === $svg) { return 'dashicons-awards'; } return 'data:image/svg+xml;base64,' . base64_encode($svg); } public function addMenuPages() { $menu_label = 'Crownify'; if (Utils::hasActivePro()) { $menu_label .= ' Pro'; } // Free builds carry core's own count bubble, the one Plugins and Comments // use. It is the only thing in the sidebar that reliably pulls the eye, // and it flags that Pro exists without spending a word on it. Menu title // only: the page title becomes the browser tab, where markup would show. $menu_title = $menu_label; if (!Utils::hasActivePro()) { $menu_title .= ' 1'; } add_menu_page($menu_label, $menu_title, 'manage_options', 'rankify', [$this, 'renderOverviewPage'], $this->menuIcon(), 55); add_submenu_page('rankify', 'Overview', 'Overview', 'manage_options', 'rankify-overview', [$this, 'renderOverviewPage'], 1); add_submenu_page('rankify', 'Redirects & Links', 'Redirects & Links', 'manage_options', 'rankify-redirects', [$this, 'renderRedirectsPage'], 2); add_submenu_page('rankify', 'Settings', 'Settings', 'manage_options', 'rankify-settings', [$this, 'renderSettingsPage'], 3); // Not in the menu: the wizard is reached from activation, the setup notice // or a direct link, and a permanent menu item for a one-time task is // clutter the moment it is done. // // The parent has to be a real menu slug. Passing null never fills the // global $title, and wp-admin/admin-header.php then hands that null to // strip_tags() on every visit. The row has to stay registered too: // remove_submenu_page() drops the parent lookup, user_can_access_admin_page() // then checks the wrong hook and answers 403. hideSetupMenuItem() keeps it // out of the sidebar. The load- hook sets the title before the header runs. $setupHook = add_submenu_page('rankify', __('Setup', 'crownify'), __('Setup', 'crownify'), 'manage_options', 'rankify-setup', [$this, 'renderSetupPage']); if (is_string($setupHook) && '' !== $setupHook) { add_action('load-' . $setupHook, [$this, 'setSetupScreenTitle']); } remove_submenu_page('rankify', 'rankify'); $this->addMenuSeparators(); } private function addMenuSeparators() { global $menu; $position = null; foreach ($menu as $key => $item) { if (isset($item[2]) && $item[2] === 'rankify') { $position = $key; break; } } if ($position !== null) { if (!isset($menu[(int) ($position - 1)])) { $menu[(int) ($position - 1)] = ['', 'read', 'separator-rankify-top', '', 'wp-menu-separator rankify']; } if (!isset($menu[(int) ($position + 1)])) { $menu[(int) ($position + 1)] = ['', 'read', 'separator-rankify-bottom', '', 'wp-menu-separator rankify']; } ksort($menu); } } /** * Give the hidden setup screen a title before admin-header.php prints it. * * Removing the submenu row (so the wizard stays out of the sidebar) also * removes the only place WordPress looks the title up. The load- hook runs * before that header, which is the call that passes the title to strip_tags(). */ public function setSetupScreenTitle() { $GLOBALS['title'] = __('Setup', 'crownify'); } /** * Keep the wizard out of the sidebar without unregistering it. * * The submenu row is what gives the screen a title and what lets an * administrator open it. Hiding the list item leaves both of those intact. */ public function hideSetupMenuItem() { echo ''; } /** * Cache-busting version for a built asset. * * RANKIFY_VERSION only changes on release, so during development every * `npm run dev` produced a new bundle at a URL the browser had already * cached — new Tailwind classes silently did not apply, which looks exactly * like a broken layout rather than a stale file. Falls back to the plugin * version when the file cannot be stat'ed. */ protected function assetVersion($relativePath) { $path = RANKIFY_DIR_PATH . $relativePath; $modified = file_exists($path) ? filemtime($path) : false; return $modified ? RANKIFY_VERSION . '.' . $modified : RANKIFY_VERSION; } public function loadAssets($hook) { wp_register_script('rankify-upgrade-menu-item-edit', false, [], RANKIFY_VERSION, true); wp_enqueue_script('rankify-upgrade-menu-item-edit'); $js = " document.addEventListener('DOMContentLoaded', function () { const fsUpgradeItems = document.querySelectorAll( 'span.fs-submenu-item.pricing' ); fsUpgradeItems.forEach(function(fsUpgradeItem) { const parentLink = fsUpgradeItem.closest('a'); fsUpgradeItem.textContent = '" . esc_js(Utils::getUpgradeLabel()) . "'; fsUpgradeItem.style.color = '#fff'; fsUpgradeItem.style.fontWeight = '500'; fsUpgradeItem.style.fontSize = '12px'; fsUpgradeItem.style.lineHeight = '1'; fsUpgradeItem.style.background = 'linear-gradient(to right, #7c3aed, #4f46e5)'; fsUpgradeItem.style.padding = '9px'; fsUpgradeItem.style.display = 'flex'; fsUpgradeItem.style.alignItems = 'center'; fsUpgradeItem.style.justifyContent = 'center'; fsUpgradeItem.style.borderRadius = '6px'; fsUpgradeItem.style.border = '1px solid transparent'; fsUpgradeItem.style.textDecoration = 'none'; }); }); "; wp_add_inline_script('rankify-upgrade-menu-item-edit', $js); if ($hook && strpos($hook, 'rankify') !== false) { remove_all_actions('admin_notices'); remove_all_actions('all_admin_notices'); $dependencies = []; $dependencies[] = 'wp-i18n'; $dependencies[] = 'jquery'; // components/settings.js reads wpApiSettings.root and .nonce to reach the // REST route. Only wp-api-request localizes that object, so without it the // auto-save works by luck — whenever something else in wp-admin happens to // enqueue it — and silently breaks when nothing does. $dependencies[] = 'wp-api-request'; // load admin styles and scripts wp_enqueue_style('rankify-admin', plugins_url('assets/css/admin.min.css', RANKIFY_FILE), [], $this->assetVersion('assets/css/admin.min.css')); wp_enqueue_script('rankify-admin', plugins_url('assets/js/admin.min.js', RANKIFY_FILE), $dependencies, $this->assetVersion('assets/js/admin.min.js'), true); wp_set_script_translations('rankify-admin', 'crownify'); $dependencies[] = 'rankify-admin'; // WP media wp_enqueue_media(); // Pass PHP variables to JS wp_localize_script( 'rankify-admin', 'rankify_admin_js_vars', apply_filters('rankify_admin_js_vars', [ 'siteName' => get_bloginfo('name'), 'homeUrl' => Utils::getHomeUrl('/', false), 'upgradeUrl' => Utils::getUpgradeUrl(), 'pluginDirUrl' => RANKIFY_DIR_URL, 'adminUrl' => trailingslashit(admin_url('/', 'https')), 'hasActivePro' => Utils::hasActivePro(), 'pricingUrl' => Utils::getPricingUrl(), 'proInfoUrl' => Utils::getProInfoUrl(), 'settings' => $this->settingsForScript(), ]), ); } } /** * Settings passed to the admin script. * * The Google client secret is a credential. The settings screen still has it * in its own form so it can be edited and saved; it does not need to be on * every Crownify screen via the localized object. */ protected function settingsForScript() { $settings = get_option('rankify_settings', []); if (!is_array($settings)) { return []; } unset($settings['searchConsoleClientSecret']); return $settings; } public function renderPage($pageId) { ?>
renderPage('overview'); } public function renderSettingsPage() { return $this->renderPage('settings'); } public function renderRedirectsPage() { return $this->renderPage('redirects'); } public function renderSetupPage() { return $this->renderPage('setup'); } }