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('