env = $env; $this->metricool = $metricool; $this->service = $service; $this->account = $account; $this->languages = $languages; } public function register(): void { // Show forced login screen when coming from legacy add_action('metricool_plugin_legacy_upgrade', [$this->service, 'enableForcedLogin']); add_action('metricool_onboarding_completed', [$this->service, 'disableForcedLogin']); // Redirect on the activation hook, but do it after anything else. add_action('metricool_activation', [$this, 'maybeRedirectToDashboard'], 9999); add_action('admin_menu', [$this, 'addDashboardPage']); } /** * Redirect to metricool dashboard page on activation, but only if the user * manually activated the plugin via the plugins overview. React will handle * redirect to onboarding if needed. * * @param string $pageSource The page where the activation was triggered, * usually 'plugins.php' but can be other pages as well. */ public function maybeRedirectToDashboard(string $pageSource = ''): void { if ($pageSource !== 'plugins.php') { return; } wp_safe_redirect($this->env->getUrl('plugin.dashboard_url')); exit; } /** * Add the dashboard page to the admin menu of WordPress. Also triggers the * action to enqueue scripts and styles * @uses apply_filters metricool_menu_position */ public function addDashboardPage(): void { /** * Filter: metricool_menu_position * Can be used to change the position of the menu item in the admin menu. * @param int $menuPosition * @return int Default 59 to be positioned after "wp-menu-separator" and * before "Appearance". */ $menuPosition = apply_filters('metricool_menu_position', 59); $pageHookSuffix = add_menu_page( esc_html__('Metricool', 'metricool'), esc_html__('Metricool', 'metricool'), 'metricool_manage', 'metricool', [$this, 'renderReactApp'], $this->getMenuItemIconSVG(), $menuPosition, ); add_action("admin_print_scripts-$pageHookSuffix", [$this, 'enqueueReactScripts']); } /** * Render the React app in the WordPress admin */ public function renderReactApp(): void { $this->render('admin/dashboard', [], 'html'); } /** * Returns the SVG icon for the menu item in the admin menu. * @return string Base64 encoded SVG image */ public function getMenuItemIconSVG(): string { $svg = ' '; return 'data:image/svg+xml;base64,' . base64_encode($svg); } /** * Enqueue the Tailwind CSS for the dashboard in the header */ public function enqueueDashboardStyles(): void { $chunkTranslation = $this->getReactChunkTranslations(); if (empty($chunkTranslation)) { return; } wp_enqueue_style( 'metricool-tailwind', $this->env->getUrl('plugin.assets_url') . '/css/tailwind.generated.css', // todo - feel free to place this in a different location [], ($chunkTranslation['version'] ?? '') ); } /** * Enqueue the React scripts and styles for the dashboard: * All files to enqueue are listed in manifest.json, generated by Vite, * and available in react/build/assets * * Load translations for the React app */ public function enqueueReactScripts(): void { $manifest = $this->env->getString('plugin.react_path') . '/build/.vite/manifest.json'; $json_translations = []; if (file_exists($manifest)) { $manifest_contents = file_get_contents($manifest); $decoded_manifest = json_decode($manifest_contents, true); foreach ($decoded_manifest as $key => $value) { if (substr($value['file'], -3) === '.js') { wp_register_script( $key, $this->env->getUrl('plugin.react_url') . '/build/' . $value['file'], [], null, false, ); $translation_data = load_script_textdomain($key, 'metricool'); if (!empty($translation_data)) { $json_translations[] = $translation_data; } if (!empty($value['isEntry']) && $value['isEntry'] === true) { wp_enqueue_script( 'metricool-main-script', $this->env->getUrl('plugin.react_url') . '/build/' . $value['file'], [], null, false, ); } } if (!empty($value['css'])) { wp_enqueue_style( 'metricool-tailwind', $this->env->getUrl('plugin.react_url') . '/build/' . $value['css'][0], [], null, ); } } } add_filter('script_loader_tag', [$this, 'loadMainScriptsAsModule'], 10, 2); wp_localize_script( 'metricool-main-script', 'metricool', ['values' => $this->localizedReactSettings(['json_translations' => $json_translations])], ); } /** * WordPress doesn't allow for translation of chunks resulting of code * splitting. Several workarounds have popped up in JetPack and Woocommerce. * Below is mainly based on the Woocommerce solution, which seems to be the * simplest approach. Simplicity is king here. * @see https://wordpress.com/blog/2022/01/06/wordpress-plugin-i18n-webpack-and-composer/ */ private function getReactChunkTranslations(): array { $cacheName = 'metricool-react-chunk-translations'; if ($cache = wp_cache_get($cacheName, 'metricool')) { return $cache; } // get all files from the settings/build folder $buildDirPath = $this->env->getString('plugin.react_path') . '/build'; $filenames = scandir($buildDirPath); $jsFileName = ''; $assetFilename = ''; $jsonTranslations = []; // filter the filenames to get the JavaScript and asset filenames foreach ($filenames as $filename) { if (strpos($filename, 'index.') === 0) { if (substr($filename, -3) === '.js') { $jsFileName = $filename; } elseif (substr($filename, -10) === '.asset.php') { $assetFilename = $filename; } } if (strpos($filename, '.js') === false) { continue; } // remove extension from $filename $chunkHandle = str_replace('.js', '', $filename); // temporarily register the script, so we can get a translations object. $chunkSource = $this->env->getUrl('plugin.react_url') . '/build/' . $filename; wp_register_script($chunkHandle, $chunkSource, [], $this->env->getString('plugin.version'), true); //as there is no pro version of this plugin, no need to declare a path $localeData = load_script_textdomain($chunkHandle, 'metricool'); if (!empty($localeData)) { $jsonTranslations[] = $localeData; } wp_deregister_script($chunkHandle); } if (empty($jsFileName)) { return []; } $assetFileData = require $buildDirPath . '/' . $assetFilename; $chunkTranslations = [ 'json_translations' => $jsonTranslations, 'js_file_name' => $jsFileName, 'dependencies' => $assetFileData['dependencies'] ?? [], 'version' => $assetFileData['version'] ?? '', ]; wp_cache_set($cacheName, $chunkTranslations, 'metricool'); return $chunkTranslations; } /** * Build the localization array for the React script with the translations * @uses apply_filters metricool_localize_dashboard_script */ private function localizedReactSettings(array $chunkTranslation): array { $settings = [ 'nonce' => wp_create_nonce('metricool_nonce'), 'x_wp_nonce' => wp_create_nonce('wp_rest'), 'ajax_url' => admin_url('admin-ajax.php'), 'rest_url' => get_rest_url(), 'rest_namespace' => $this->env->getString('http.namespace'), 'rest_version' => $this->env->getString('http.version'), 'api_url' => trailingslashit( get_rest_url(null, $this->env->getString('http.namespace') . '/' . $this->env->getString('http.version')) ), 'dashboard_url' => $this->env->getString('plugin.dashboard_url'), 'site_url' => site_url(), 'assets_url' => $this->env->getUrl('plugin.assets_url'), 'json_translations' => ($chunkTranslation['json_translations'] ?? []), 'trusted_urls' => $this->env->get('frontend.trusted_urls'), 'onboarding' => [ 'state' => $this->service->state(), 'mode' => $this->service->mode(), ], 'support' => $this->env->getUrl('metricool.support'), 'metricool_base_url' => $this->env->getUrl('metricool.base_url'), 'metricool_help_url' => $this->env->getUrl('metricool.help_url'), 'locale' => str_replace("_", "-", get_user_locale()), 'supported_languages' => $this->languages->all(), 'google_recaptcha_url' => $this->getGoogleRecaptchaUrl(), 'google_recaptcha_key' => $this->env->getString('metricool.google_recaptcha_key'), ]; if ($this->metricool->hasAuthentication()) { $settings['account'] = $this->account->fetch(); } return apply_filters('metricool_localize_dashboard_script', $settings); } /** * Get the Google reCAPTCHA URL with the key from the environment config. */ private function getGoogleRecaptchaUrl(): string { return 'https://www.google.com/recaptcha/enterprise.js?render=' . $this->env->getString('metricool.google_recaptcha_key'); } public function loadMainScriptsAsModule(string $tag, string $handle): string { if (str_contains($handle, 'metricool-main-script') === false) { return $tag; } return str_replace('