| @@ -155,11 +155,18 @@ | ||
| 155 | 155 | $asset_version |
| 156 | 156 | ); |
| 157 | 157 | |
| 158 | 158 | // ── React app styles ── |
| 159 | + // Version with the same build hash as the JS bundle (not the static | |
| 160 | + // plugin version) so a rebuild that doesn't bump ZIPAI_MCP_VERSION still | |
| 161 | + // busts the CSS cache. Otherwise `?ver=<plugin version>` stays identical | |
| 162 | + // across builds and browsers/CDNs serve a stale stylesheet against fresh | |
| 163 | + // JS — newly added utility classes go missing until the version bumps. | |
| 159 | 164 | $this->style_operations( |
| 160 | 165 | 'chat-assistant', |
| 161 | - $this->build_url . 'css/dist/chat-assistant.css' | |
| 166 | + $this->build_url . 'css/dist/chat-assistant.css', | |
| 167 | + array(), | |
| 168 | + $asset_version | |
| 162 | 169 | ); |
| 163 | 170 | |
| 164 | 171 | // ── Gutenberg editor plugin (native post/page editor only) ── |
| 165 | 172 | // Uses the same narrowed gate as `isBlockEditor` so the editor RPC |
| @@ -195,33 +202,16 @@ | ||
| 195 | 202 | 'tool-hooks', |
| 196 | 203 | 'zipwpIframeConfig', |
| 197 | 204 | array( |
| 198 | 205 | 'nonce' => wp_create_nonce( 'zip_ai_iframe' ), |
| 199 | - 'restNonce' => wp_create_nonce( 'wp_rest' ), | |
| 200 | 206 | 'ajaxUrl' => admin_url( 'admin-ajax.php' ), |
| 201 | 207 | 'displayMode' => $this->is_fullpage_screen() ? 'fullpage' : 'sidebar', |
| 202 | 208 | 'adminHomeUrl' => admin_url(), |
| 203 | 209 | 'userId' => get_current_user_id(), |
| 204 | 210 | 'authUrl' => $auth_url, |
| 205 | - 'websiteContext' => array( | |
| 206 | - 'site_url' => get_site_url(), | |
| 207 | - 'admin_url' => admin_url(), | |
| 208 | - 'site_title' => get_bloginfo( 'name' ), | |
| 209 | - 'site_tagline' => get_bloginfo( 'description' ), | |
| 210 | - 'language' => get_bloginfo( 'language' ), | |
| 211 | - 'timezone' => wp_timezone_string(), | |
| 212 | - 'date_format' => get_option( 'date_format' ), | |
| 213 | - 'time_format' => get_option( 'time_format' ), | |
| 214 | - 'is_multisite' => is_multisite(), | |
| 215 | - ), | |
| 216 | - 'pageContext' => $this->get_current_page_context(), | |
| 217 | 211 | 'activeProduct' => Product_Context::detect(), |
| 218 | 212 | 'isBlockEditor' => $this->is_block_editor_screen(), |
| 219 | 213 | 'isPostEditScreen' => $this->is_post_edit_screen(), |
| 220 | - 'themeContext' => array( | |
| 221 | - 'color_palette' => $this->get_theme_color_palette(), | |
| 222 | - ), | |
| 223 | - 'installedPlugins' => $this->get_installed_plugins_versions(), | |
| 224 | 214 | 'setupGate' => $this->get_setup_gate(), |
| 225 | 215 | ) |
| 226 | 216 | ); |
| 227 | 217 | |
| @@ -248,12 +238,28 @@ | ||
| 248 | 238 | if ( ! is_array( $brand_context ) ) { |
| 249 | 239 | $brand_context = array(); |
| 250 | 240 | } |
| 251 | 241 | |
| 242 | + // Free/guest temp-site expiry (ISO-8601 UTC). The zipwp-client mu-plugin | |
| 243 | + // stores it in the `zipwp_site_data` option and we suppress ZipWP's own | |
| 244 | + // admin-bar countdown when ZIP AI owns the surface, so the assistant | |
| 245 | + // surfaces the timer itself. Absent on permanent/paid sites, and | |
| 246 | + // suppressed once the site is reserved (no longer expiring). | |
| 247 | + $zipwp_site_data = get_option( 'zipwp_site_data' ); | |
| 248 | + $expire_at = ( is_array( $zipwp_site_data ) && empty( $zipwp_site_data['reserve'] ) && ! empty( $zipwp_site_data['expire_at'] ) ) | |
| 249 | + ? $zipwp_site_data['expire_at'] | |
| 250 | + : null; | |
| 251 | + | |
| 252 | 252 | $this->localize_script( |
| 253 | 253 | 'chat-assistant', |
| 254 | 254 | 'ZIPAI_CONFIG', |
| 255 | 255 | array( |
| 256 | + // Turns on the bridge/apply-change console mirror (wp-bridge-host.js, | |
| 257 | + // apply-change/handler.js). Those log lines were already written and | |
| 258 | + // gated on this flag, but nothing ever set it — so the whole tracing | |
| 259 | + // surface has been dead. Follows WP_DEBUG; override with the filter | |
| 260 | + // for a UAT run on a site that isn't in debug mode. | |
| 261 | + 'debug' => (bool) apply_filters( 'zipai_frontend_debug', defined( 'WP_DEBUG' ) && WP_DEBUG ), | |
| 256 | 262 | 'apiUrl' => rtrim( ZIPAI_MCP_CREDIT_SERVER_API, '/' ), |
| 257 | 263 | // Server base URL for direct calls (e.g. inline-edit). Set via the |
| 258 | 264 | // ZIPAI_BRAIN_URL constant (defined in loader.php); override in |
| 259 | 265 | // wp-config.php. |
| @@ -269,10 +275,10 @@ | ||
| 269 | 275 | // the menu entry is hidden for anyone who would only get a |
| 270 | 276 | // permission error after clicking it. |
| 271 | 277 | 'canManageConnection' => current_user_can( 'manage_options' ), |
| 272 | 278 | 'displayMode' => $this->is_fullpage_screen() ? 'fullpage' : 'sidebar', |
| 279 | + 'adminHomeUrl' => admin_url(), | |
| 273 | 280 | 'fullPageUrl' => admin_url( 'options-general.php?page=zip-ai-assistant' ), |
| 274 | - 'adminHomeUrl' => admin_url(), | |
| 275 | 281 | 'userId' => get_current_user_id(), |
| 276 | 282 | 'domain' => wp_parse_url( home_url(), PHP_URL_HOST ), |
| 277 | 283 | 'site_url' => home_url(), |
| 278 | 284 | 'user' => array( |
| @@ -279,9 +285,9 @@ | ||
| 279 | 285 | 'id' => get_current_user_id(), |
| 280 | 286 | 'email' => Helper::get_setting( 'user_email', '' ), |
| 281 | 287 | 'name' => Helper::get_setting( 'user_name', '' ), |
| 282 | 288 | ), |
| 283 | - 'isFreshSite' => (bool) get_option( 'fresh_site', false ), | |
| 289 | + 'expireAt' => $expire_at, | |
| 284 | 290 | // Brand context for on-brand palette generation (colour picker). |
| 285 | 291 | 'brandContext' => $brand_context, |
| 286 | 292 | 'nonce' => wp_create_nonce( 'zip_ai_iframe' ), |
| 287 | 293 | // wp_rest nonce — required by browser-side code that calls WP |
| @@ -300,9 +306,8 @@ | ||
| 300 | 306 | // ZipWP chat loads. (Theme/plugin lifecycle MCP tools now run |
| 301 | 307 | // server-side and do not use this.) |
| 302 | 308 | 'updatesNonce' => wp_create_nonce( 'updates' ), |
| 303 | 309 | 'ajaxUrl' => admin_url( 'admin-ajax.php' ), |
| 304 | - 'authUrl' => $auth_url, | |
| 305 | 310 | ) |
| 306 | 311 | ); |
| 307 | 312 | } |
| 308 | 313 | |
| @@ -452,48 +457,8 @@ | ||
| 452 | 457 | $this->enqueue_script( 'bridge-host' ); |
| 453 | 458 | } |
| 454 | 459 | |
| 455 | 460 | /** |
| 456 | - * Get current page/post context from PHP. | |
| 457 | - * | |
| 458 | - * @since 1.0.0 | |
| 459 | - * @return array<string, int|string|null> | |
| 460 | - */ | |
| 461 | - private function get_current_page_context() { | |
| 462 | - global $post; | |
| 463 | - | |
| 464 | - $context = array( | |
| 465 | - 'post_id' => null, | |
| 466 | - 'post_type' => null, | |
| 467 | - 'post_title' => null, | |
| 468 | - 'post_status' => null, | |
| 469 | - ); | |
| 470 | - | |
| 471 | - if ( $post instanceof \WP_Post ) { | |
| 472 | - $context['post_id'] = $post->ID; | |
| 473 | - $context['post_type'] = $post->post_type; | |
| 474 | - $context['post_title'] = $post->post_title; | |
| 475 | - $context['post_status'] = $post->post_status; | |
| 476 | - return $context; | |
| 477 | - } | |
| 478 | - | |
| 479 | - if ( is_admin() ) { | |
| 480 | - $post_id = isset( $_GET['post'] ) && is_scalar( $_GET['post'] ) ? absint( $_GET['post'] ) : 0; // phpcs:ignore WordPress.Security.NonceVerification.Recommended | |
| 481 | - if ( $post_id ) { | |
| 482 | - $admin_post = get_post( $post_id ); | |
| 483 | - if ( $admin_post instanceof \WP_Post ) { | |
| 484 | - $context['post_id'] = $admin_post->ID; | |
| 485 | - $context['post_type'] = $admin_post->post_type; | |
| 486 | - $context['post_title'] = $admin_post->post_title; | |
| 487 | - $context['post_status'] = $admin_post->post_status; | |
| 488 | - } | |
| 489 | - } | |
| 490 | - } | |
| 491 | - | |
| 492 | - return $context; | |
| 493 | - } | |
| 494 | - | |
| 495 | - /** | |
| 496 | 461 | * Authoritative server-side check for whether the current screen is the |
| 497 | 462 | * NATIVE WordPress post/page editor (post.php / post-new.php, screen base |
| 498 | 463 | * 'post'). The JS bridge falls back to this when `wp.data` / |
| 499 | 464 | * `core/block-editor` are not yet initialized at iframe boot — without |
| @@ -575,73 +540,8 @@ | ||
| 575 | 540 | $auth_middleware |
| 576 | 541 | ); |
| 577 | 542 | |
| 578 | 543 | return $auth_url; |
| 579 | - } | |
| 580 | - | |
| 581 | - /** | |
| 582 | - * Get theme color palette formatted as CSS variables. | |
| 583 | - * | |
| 584 | - * @since 1.0.0 | |
| 585 | - * @return array<string, string> | |
| 586 | - */ | |
| 587 | - private function get_theme_color_palette() { | |
| 588 | - if ( ! function_exists( 'astra_get_palette_colors' ) ) { | |
| 589 | - return array(); | |
| 590 | - } | |
| 591 | - | |
| 592 | - $palette_data = astra_get_palette_colors(); | |
| 593 | - if ( ! is_array( $palette_data ) ) { | |
| 594 | - return array(); | |
| 595 | - } | |
| 596 | - | |
| 597 | - $current_palette = is_string( $palette_data['currentPalette'] ?? null ) ? $palette_data['currentPalette'] : ''; | |
| 598 | - $palettes = is_array( $palette_data['palettes'] ?? null ) ? $palette_data['palettes'] : array(); | |
| 599 | - | |
| 600 | - if ( '' === $current_palette || empty( $palettes[ $current_palette ] ) ) { | |
| 601 | - return array(); | |
| 602 | - } | |
| 603 | - | |
| 604 | - $colors = $palettes[ $current_palette ]; | |
| 605 | - if ( ! is_array( $colors ) ) { | |
| 606 | - return array(); | |
| 607 | - } | |
| 608 | - | |
| 609 | - $formatted_palette = array(); | |
| 610 | - | |
| 611 | - foreach ( $colors as $index => $color ) { | |
| 612 | - if ( ! is_scalar( $color ) ) { | |
| 613 | - continue; | |
| 614 | - } | |
| 615 | - $formatted_palette[ '--ast-global-color-' . $index ] = (string) $color; | |
| 616 | - } | |
| 617 | - | |
| 618 | - return $formatted_palette; | |
| 619 | - } | |
| 620 | - | |
| 621 | - /** | |
| 622 | - * Get installed plugins with their versions. | |
| 623 | - * | |
| 624 | - * @since 1.0.0 | |
| 625 | - * @return array<string, string> | |
| 626 | - */ | |
| 627 | - private function get_installed_plugins_versions() { | |
| 628 | - if ( ! function_exists( 'get_plugins' ) ) { | |
| 629 | - require_once ABSPATH . 'wp-admin/includes/plugin.php'; | |
| 630 | - } | |
| 631 | - | |
| 632 | - $all_plugins = get_plugins(); | |
| 633 | - $plugin_versions = array(); | |
| 634 | - | |
| 635 | - foreach ( $all_plugins as $plugin_file => $plugin_data ) { | |
| 636 | - $slug = dirname( $plugin_file ); | |
| 637 | - if ( '.' === $slug ) { | |
| 638 | - $slug = basename( $plugin_file, '.php' ); | |
| 639 | - } | |
| 640 | - $plugin_versions[ $slug ] = is_string( $plugin_data['Version'] ?? null ) ? $plugin_data['Version'] : '0.0.0'; | |
| 641 | - } | |
| 642 | - | |
| 643 | - return $plugin_versions; | |
| 644 | 544 | } |
| 645 | 545 | |
| 646 | 546 | /** |
| 647 | 547 | * Build the Spectra setup-gate descriptor for the React notice. |