build entry name, for the screens we enqueue on. * * @var array */ private const SCREENS = array( 'toplevel_page_' . Menu::DASHBOARD_SLUG => 'dashboard', 'tagsmith_page_' . Menu::SETTINGS_SLUG => 'settings', ); /** * Option holding the encrypted Clarity Data Export token. Only whether * it is set is ever read here; the token itself is never localized. * * @var string */ private const CLARITY_TOKEN_OPTION = 'tagsmith_clarity_token'; /** * Constructor. * * @param Plugin $plugin Plugin service locator. */ public function __construct( Plugin $plugin ) { $this->plugin = $plugin; } /** * Registers the admin_enqueue_scripts hook. */ public function register(): void { add_action( 'admin_enqueue_scripts', array( $this, 'enqueue' ) ); } /** * Enqueues the entry for the current screen, if it is one of ours. * * @param string $hook_suffix The current admin page's hook suffix. */ public function enqueue( string $hook_suffix ): void { if ( ! array_key_exists( $hook_suffix, self::SCREENS ) ) { return; } $entry = self::SCREENS[ $hook_suffix ]; $this->enqueue_entry( $entry ); } /** * Enqueues one build entry: its script (with its asset.php deps and * version), its style, and the inline tagsmithConfig. * * @param string $entry Entry name (matches webpack.config.js and build/*.asset.php). */ private function enqueue_entry( string $entry ): void { $handle = 'tagsmith-' . $entry; $asset_php = TAGSMITH_DIR . 'build/' . $entry . '.asset.php'; if ( ! is_readable( $asset_php ) ) { return; } $asset = require $asset_php; wp_enqueue_script( $handle, TAGSMITH_URL . 'build/' . $entry . '.js', (array) ( $asset['dependencies'] ?? array() ), (string) ( $asset['version'] ?? TAGSMITH_VERSION ), true ); wp_set_script_translations( $handle, 'tagsmith' ); wp_enqueue_style( 'wp-components' ); $css_path = TAGSMITH_DIR . 'build/' . $entry . '.css'; if ( is_readable( $css_path ) ) { wp_enqueue_style( $handle, TAGSMITH_URL . 'build/' . $entry . '.css', array( 'wp-components' ), (string) ( $asset['version'] ?? TAGSMITH_VERSION ) ); } $config = $this->config(); // The ad pixel keys are only for the settings screen, where the Ads & pixels tab reads them. if ( 'settings' === $entry ) { $config = array_merge( $config, $this->pixel_config() ); } wp_add_inline_script( $handle, // This is printed inside a script block: < > and & leave as \u escapes. 'window.tagsmithConfig = ' . wp_json_encode( $config, JSON_HEX_TAG | JSON_HEX_AMP ) . ';', 'before' ); } /** * The config every Tagsmith JS entry (dashboard, settings, editor, * widget) reads from `window.tagsmithConfig`. Never includes * secrets: only ids, names and booleans already safe to display. * * Microsoft Clarity: clarityEnabled (the tag is on, with a project id), * clarityProjectId, clarityHost (the host Clarity records, for deep-link * URL filters), clarityInsights (a Data Export token is saved) and * clarityDetectedProjectId (the project id Microsoft's Clarity plugin * stores, offered while ours is empty). * * Consent: consentApiActive (a consent plugin that speaks the WP Consent * API is active, so the Privacy settings can say so). * * @return array */ public function config(): array { $settings = $this->plugin->settings(); $connection = $this->plugin->connection(); $clarity_id = (string) $settings->get( 'clarity_project_id' ); return array( 'version' => TAGSMITH_VERSION, 'restNamespace' => Controller::REST_NAMESPACE, 'dashboardUrl' => admin_url( 'admin.php?page=' . Menu::DASHBOARD_SLUG ), 'settingsUrl' => admin_url( 'admin.php?page=' . Menu::SETTINGS_SLUG ), 'gaUrl' => self::ga_url( (string) $settings->get( 'property_id' ) ), 'connected' => $connection->is_connected(), 'reconnectRequired' => $connection->reconnect_required(), 'onboarded' => (bool) $settings->get( 'onboarded' ), 'canManage' => current_user_can( 'manage_options' ), 'gscEnabled' => (bool) $settings->get( 'gsc_enabled' ), 'propertyName' => (string) $settings->get( 'property_name' ), 'propertyId' => (string) $settings->get( 'property_id' ), 'demo' => defined( 'TAGSMITH_DEMO' ) && TAGSMITH_DEMO, 'relayHost' => (string) wp_parse_url( Plugin::relay_url(), PHP_URL_HOST ), 'relayIsDefault' => Plugin::relay_is_default(), 'oauthAppDomain' => Plugin::oauth_app_domain(), 'redirectUri' => OAuthFlow::return_url(), 'postTypes' => array_values( (array) $settings->get( 'post_types' ) ), 'earliestDate' => DateRange::earliest_date(), 'siteToday' => DateRange::site_today(), 'clarityEnabled' => (bool) $settings->get( 'clarity_enabled' ) && '' !== $clarity_id, 'clarityProjectId' => $clarity_id, 'clarityHost' => self::clarity_host( (string) $settings->get( 'stream_default_uri' ) ), 'clarityInsights' => ! empty( get_option( self::CLARITY_TOKEN_OPTION, '' ) ), 'clarityDetectedProjectId' => '' === $clarity_id ? Settings::microsoft_clarity_project_id() : '', 'consentApiActive' => self::consent_api_active(), ); } /** * What the settings screen's Ads & pixels tab reads from * `window.tagsmithConfig`, on top of config(): the platform registry * (`pixelPlatforms`, see Platforms::ui_payload()), the consent banner the * server found (`consentBanner`: `{ type, name }`, `type` being * `wp-consent-api`, `browser` for a banner that decides only in the * browser, or `none`), the last Health scan of the homepage * (`pixelScan`, or null; see HealthCheck::PIXEL_SCAN_OPTION) and the link * that opens the site in preview (`pixelPreviewUrl`, with a nonce, for a * user who can manage options, else an empty string; also empty in the * demo, which never loads a pixel). * * @return array{pixelPlatforms: list>, consentBanner: array{type: string, name: string}, pixelScan: array|null, pixelPreviewUrl: string} */ public function pixel_config(): array { return array( 'pixelPlatforms' => Platforms::ui_payload(), 'consentBanner' => $this->plugin->server_consent()->banner(), 'pixelScan' => HealthCheck::stored_pixel_scan(), 'pixelPreviewUrl' => current_user_can( 'manage_options' ) && ! Plugin::is_demo() ? $this->plugin->pixel_preview()->url() : '', ); } /** * Whether a consent plugin that speaks the WP Consent API is active: the * API's wp_has_consent() function exists. The same test TagRenderer uses * to print the consent bridge and HealthCheck uses to recommend a * banner. */ public static function consent_api_active(): bool { return function_exists( 'wp_has_consent' ); } /** * The host Clarity records page URLs under: the GA web stream's host * when one is set (the live site, e.g. when wp-admin runs on a staging * copy), else the home URL's host. Lower-cased. * * @param string $stream_uri The GA web stream's default URI, or ''. */ private static function clarity_host( string $stream_uri ): string { $host = '' === $stream_uri ? '' : (string) wp_parse_url( $stream_uri, PHP_URL_HOST ); if ( '' === $host ) { $host = (string) wp_parse_url( home_url(), PHP_URL_HOST ); } return strtolower( $host ); } /** * The "View in Google Analytics" deep link: the property's Intelligence * Home report when a property is connected, otherwise the GA4 base URL. * * @param string $property_id 'properties/123' or ''. */ private static function ga_url( string $property_id ): string { $base = 'https://analytics.google.com/analytics/web/'; if ( 1 !== preg_match( '#^properties/(\d+)$#', $property_id, $matches ) ) { return $base; } return $base . '#/p' . $matches[1] . '/reports/intelligenthome'; } }