Changeset 3737499
- Timestamp:
- 10/10/2026 01:28:36 AM (2 weeks ago)
- Location:
- noahfirst-ai-assistant
- Files:
-
- 4 edited
- 1 copied
-
tags/1.1.0 (copied) (copied from noahfirst-ai-assistant/trunk )
-
tags/1.1.0/noahfirst-ai-assistant.php (modified) (4 diffs)
-
tags/1.1.0/readme.txt (modified) (2 diffs)
-
trunk/noahfirst-ai-assistant.php (modified) (4 diffs)
-
trunk/readme.txt (modified) (2 diffs)
Legend:
- Unmodified
- Added
- Removed
-
noahfirst-ai-assistant/tags/1.1.0/noahfirst-ai-assistant.php
r3534632 r3737499 2 2 3 3 /** 4 * Plugin Name: NoahFirst AI Assistant5 * Description: Boost your ecommerce sales with an AI chatbot that understands your store data and turns conversations into customers.6 * Version: 1. 0.04 * Plugin Name: NoahFirst AI Sales Associate 5 * Description: AI sales associate for online stores that answers buyer questions using your own store data. 6 * Version: 1.1.0 7 7 * License: GPLv2 or later 8 8 * Author: NoahFirstAI … … 12 12 defined('ABSPATH') || exit; 13 13 14 // Add NoahFirst Widget script 14 const NOAHFIRST_AI_ASSISTANT_SYNC_HOOK = 'noahfirst_ai_assistant_run_store_sync'; 15 16 function noahfirst_ai_assistant_get_api_url() { 17 return apply_filters( 18 'noahfirst_ai_assistant_api_url', 19 'https://api.noahfirst.com/graphql' 20 ); 21 } 22 23 function noahfirst_ai_assistant_get_widget_script_url() { 24 return apply_filters( 25 'noahfirst_ai_assistant_widget_script_url', 26 'https://api.noahfirst.com/assets/scripts/noah/prod.tanyanoah.v0.0.2.js' 27 ); 28 } 29 30 function noahfirst_ai_assistant_graphql_request($query, $variables, $secret_key, $operation_name) { 31 $response = wp_remote_post( 32 noahfirst_ai_assistant_get_api_url(), 33 array( 34 'method' => 'POST', 35 'timeout' => 60, 36 'redirection' => 3, 37 'headers' => array( 38 'Content-Type' => 'application/json', 39 'x-noahfirst-secret-key' => $secret_key, 40 'x-apollo-operation-name' => $operation_name, 41 'apollo-require-preflight' => 'true', 42 ), 43 'body' => wp_json_encode( 44 array( 45 'query' => $query, 46 'variables' => $variables, 47 ) 48 ), 49 ) 50 ); 51 52 if (is_wp_error($response)) { 53 return false; 54 } 55 56 $status_code = wp_remote_retrieve_response_code($response); 57 $body = wp_remote_retrieve_body($response); 58 $decoded = json_decode($body, true); 59 60 if ( 61 $status_code < 200 || 62 $status_code >= 300 || 63 !is_array($decoded) || 64 !empty($decoded['errors']) 65 ) { 66 return false; 67 } 68 69 return $decoded; 70 } 71 72 function noahfirst_ai_assistant_format_product_attributes($product) { 73 $formatted = array(); 74 75 if (!$product instanceof WC_Product) { 76 return $formatted; 77 } 78 79 foreach ($product->get_attributes() as $key => $attribute) { 80 if ($attribute instanceof WC_Product_Attribute) { 81 $name = $attribute->get_name(); 82 $values = array(); 83 84 if ($attribute->is_taxonomy()) { 85 $terms = wc_get_product_terms( 86 $product->get_id(), 87 $name, 88 array('fields' => 'names') 89 ); 90 if (!is_wp_error($terms)) { 91 $values = array_values($terms); 92 } 93 } else { 94 $values = array_values($attribute->get_options()); 95 } 96 97 $formatted[$name] = $values; 98 continue; 99 } 100 101 if (is_array($attribute)) { 102 $formatted[(string) $key] = array_values($attribute); 103 continue; 104 } 105 106 $formatted[(string) $key] = array((string) $attribute); 107 } 108 109 return $formatted; 110 } 111 112 function noahfirst_ai_assistant_format_variation_attributes($variation) { 113 $formatted = array(); 114 115 if (!$variation instanceof WC_Product_Variation) { 116 return $formatted; 117 } 118 119 foreach ($variation->get_attributes() as $key => $value) { 120 $formatted[(string) $key] = array((string) $value); 121 } 122 123 return $formatted; 124 } 125 126 function noahfirst_ai_assistant_get_product_variations($product) { 127 $variations = array(); 128 129 if ( 130 !$product instanceof WC_Product_Variable || 131 !method_exists($product, 'get_children') 132 ) { 133 return $variations; 134 } 135 136 foreach ($product->get_children() as $variation_id) { 137 $variation = wc_get_product($variation_id); 138 139 if ( 140 !$variation instanceof WC_Product_Variation || 141 $variation->get_status() !== 'publish' 142 ) { 143 continue; 144 } 145 146 $modified = $variation->get_date_modified(); 147 148 $variations[] = array( 149 'id' => $variation->get_id(), 150 'sku' => $variation->get_sku(), 151 'status' => $variation->get_status(), 152 'price' => $variation->get_price(), 153 'regular_price' => $variation->get_regular_price(), 154 'sale_price' => $variation->get_sale_price(), 155 'stock_status' => $variation->get_stock_status(), 156 'stock_quantity' => $variation->get_stock_quantity(), 157 'manage_stock' => $variation->get_manage_stock(), 158 'backorders' => $variation->get_backorders(), 159 'attributes' => noahfirst_ai_assistant_format_variation_attributes($variation), 160 'permalink' => get_permalink($product->get_id()), 161 'date_modified' => $modified ? $modified->date('c') : null, 162 ); 163 } 164 165 return $variations; 166 } 167 168 function noahfirst_ai_assistant_get_products_payload() { 169 $products = array(); 170 171 if (!class_exists('WooCommerce')) { 172 return $products; 173 } 174 175 $product_query = new WC_Product_Query( 176 array( 177 'limit' => -1, 178 'status' => 'publish', 179 'orderby' => 'title', 180 'order' => 'ASC', 181 ) 182 ); 183 184 $items = $product_query->get_products(); 185 $currency = get_woocommerce_currency(); 186 187 foreach ($items as $product) { 188 if (!$product instanceof WC_Product) { 189 continue; 190 } 191 192 $category_names = array(); 193 $terms = get_the_terms($product->get_id(), 'product_cat'); 194 195 if (!empty($terms) && !is_wp_error($terms)) { 196 foreach ($terms as $term) { 197 $category_names[] = $term->name; 198 } 199 } 200 201 $modified = $product->get_date_modified(); 202 203 $products[] = array( 204 'id' => $product->get_id(), 205 'name' => $product->get_name(), 206 'slug' => $product->get_slug(), 207 'sku' => $product->get_sku(), 208 'type' => $product->get_type(), 209 'status' => $product->get_status(), 210 'currency' => $currency, 211 'price' => $product->get_price(), 212 'regular_price' => $product->get_regular_price(), 213 'sale_price' => $product->get_sale_price(), 214 'stock_status' => $product->get_stock_status(), 215 'stock_quantity' => $product->get_stock_quantity(), 216 'manage_stock' => $product->get_manage_stock(), 217 'backorders' => $product->get_backorders(), 218 'description' => wp_strip_all_tags($product->get_description()), 219 'short_description' => wp_strip_all_tags($product->get_short_description()), 220 'permalink' => get_permalink($product->get_id()), 221 'categories' => $category_names, 222 'attributes' => noahfirst_ai_assistant_format_product_attributes($product), 223 'variations' => noahfirst_ai_assistant_get_product_variations($product), 224 'date_modified' => $modified ? $modified->date('c') : null, 225 ); 226 } 227 228 return $products; 229 } 230 231 function noahfirst_ai_assistant_get_posts_payload() { 232 $posts = get_posts( 233 array( 234 'post_type' => 'post', 235 'post_status' => 'publish', 236 'posts_per_page' => -1, 237 'orderby' => 'title', 238 'order' => 'ASC', 239 'suppress_filters' => false, 240 ) 241 ); 242 243 $payload = array(); 244 245 foreach ($posts as $post) { 246 $payload[] = array( 247 'id' => $post->ID, 248 'name' => $post->post_title, 249 'slug' => $post->post_name, 250 'type' => $post->post_type, 251 'status' => $post->post_status, 252 'description' => wp_strip_all_tags($post->post_content), 253 'short_description' => wp_strip_all_tags($post->post_excerpt), 254 'permalink' => get_permalink($post->ID), 255 'categories' => wp_get_post_categories( 256 $post->ID, 257 array('fields' => 'names') 258 ), 259 'date_modified' => get_post_modified_time('c', true, $post), 260 ); 261 } 262 263 return $payload; 264 } 265 266 function noahfirst_ai_assistant_push_catalog($token, $secret_key, $products) { 267 $query = <<<'GRAPHQL' 268 mutation IngestCommerceCatalog($input: CommerceCatalogIngestionInput!) { 269 ingestCommerceCatalogByToken(input: $input) { 270 accepted 271 status 272 product_count 273 variant_count 274 last_success_at 275 has_last_good 276 } 277 } 278 GRAPHQL; 279 280 $catalog = array( 281 'schema_version' => 1, 282 'source' => 'wordpress', 283 'generated_at' => gmdate('c'), 284 'products' => $products, 285 ); 286 287 return noahfirst_ai_assistant_graphql_request( 288 $query, 289 array( 290 'input' => array( 291 'token' => (string) $token, 292 'platform' => 'wordpress', 293 'site_url' => home_url('/'), 294 'generated_at' => gmdate('c'), 295 'catalog_json' => wp_json_encode($catalog), 296 ), 297 ), 298 $secret_key, 299 'IngestCommerceCatalog' 300 ); 301 } 302 303 function noahfirst_ai_assistant_push_posts($token, $secret_key, $posts) { 304 $query = <<<'GRAPHQL' 305 mutation IngestPlatformContent($input: PlatformContentIngestionInput!) { 306 ingestPlatformContentByToken(input: $input) { 307 accepted 308 content_type 309 item_count 310 } 311 } 312 GRAPHQL; 313 314 return noahfirst_ai_assistant_graphql_request( 315 $query, 316 array( 317 'input' => array( 318 'token' => (string) $token, 319 'platform' => 'wordpress', 320 'site_url' => home_url('/'), 321 'content_type' => 'posts', 322 'content_json' => wp_json_encode( 323 array( 324 'schema_version' => 1, 325 'source' => 'wordpress', 326 'generated_at' => gmdate('c'), 327 'posts' => $posts, 328 ) 329 ), 330 ), 331 ), 332 $secret_key, 333 'IngestPlatformContent' 334 ); 335 } 336 337 function noahfirst_ai_assistant_sync_store_to_api() { 338 $token = trim( 339 (string) get_option('noahfirst_ai_assistant_token', '') 340 ); 341 $secret_key = trim( 342 (string) noahfirst_ai_assistant_get_secret_key() 343 ); 344 345 if (empty($token) || empty($secret_key)) { 346 return false; 347 } 348 349 // Empty payloads are intentionally sent so a store that deletes its last 350 // product/post does not leave stale knowledge behind in NoahFirst. 351 $products_ok = (bool) noahfirst_ai_assistant_push_catalog( 352 $token, 353 $secret_key, 354 noahfirst_ai_assistant_get_products_payload() 355 ); 356 357 $posts_ok = (bool) noahfirst_ai_assistant_push_posts( 358 $token, 359 $secret_key, 360 noahfirst_ai_assistant_get_posts_payload() 361 ); 362 363 $success = $products_ok && $posts_ok; 364 365 update_option( 366 'noahfirst_ai_assistant_last_sync_at', 367 gmdate('c'), 368 false 369 ); 370 update_option( 371 'noahfirst_ai_assistant_last_sync_status', 372 $success ? 'success' : 'error', 373 false 374 ); 375 376 return $success; 377 } 378 379 function noahfirst_ai_assistant_schedule_sync($delay = 20) { 380 if ( 381 !get_option('noahfirst_ai_assistant_token', '') || 382 !get_option('noahfirst_ai_assistant_secret_key', '') 383 ) { 384 return; 385 } 386 387 if (!wp_next_scheduled(NOAHFIRST_AI_ASSISTANT_SYNC_HOOK)) { 388 wp_schedule_single_event( 389 time() + max(1, (int) $delay), 390 NOAHFIRST_AI_ASSISTANT_SYNC_HOOK 391 ); 392 } 393 } 394 395 add_action( 396 NOAHFIRST_AI_ASSISTANT_SYNC_HOOK, 397 'noahfirst_ai_assistant_sync_store_to_api' 398 ); 399 400 const NOAHFIRST_AI_ASSISTANT_HEARTBEAT_HOOK = 'noahfirst_ai_assistant_store_sync_heartbeat'; 401 402 function noahfirst_ai_assistant_register_heartbeat() { 403 if (!wp_next_scheduled(NOAHFIRST_AI_ASSISTANT_HEARTBEAT_HOOK)) { 404 wp_schedule_event( 405 time() + HOUR_IN_SECONDS, 406 'twicedaily', 407 NOAHFIRST_AI_ASSISTANT_HEARTBEAT_HOOK 408 ); 409 } 410 } 411 412 function noahfirst_ai_assistant_clear_heartbeat() { 413 $timestamp = wp_next_scheduled(NOAHFIRST_AI_ASSISTANT_HEARTBEAT_HOOK); 414 415 while ($timestamp) { 416 wp_unschedule_event( 417 $timestamp, 418 NOAHFIRST_AI_ASSISTANT_HEARTBEAT_HOOK 419 ); 420 $timestamp = wp_next_scheduled(NOAHFIRST_AI_ASSISTANT_HEARTBEAT_HOOK); 421 } 422 } 423 424 register_activation_hook( 425 __FILE__, 426 'noahfirst_ai_assistant_register_heartbeat' 427 ); 428 register_deactivation_hook( 429 __FILE__, 430 'noahfirst_ai_assistant_clear_heartbeat' 431 ); 432 433 add_action( 434 'init', 435 'noahfirst_ai_assistant_register_heartbeat' 436 ); 437 438 add_action( 439 NOAHFIRST_AI_ASSISTANT_HEARTBEAT_HOOK, 440 'noahfirst_ai_assistant_sync_store_to_api' 441 ); 442 443 function noahfirst_ai_assistant_schedule_woocommerce_sync() { 444 noahfirst_ai_assistant_schedule_sync(5); 445 } 446 447 add_action( 448 'woocommerce_update_product', 449 'noahfirst_ai_assistant_schedule_woocommerce_sync', 450 20, 451 1 452 ); 453 add_action( 454 'woocommerce_new_product', 455 'noahfirst_ai_assistant_schedule_woocommerce_sync', 456 20, 457 1 458 ); 459 add_action( 460 'woocommerce_product_set_stock', 461 'noahfirst_ai_assistant_schedule_woocommerce_sync', 462 20, 463 1 464 ); 465 add_action( 466 'woocommerce_variation_set_stock', 467 'noahfirst_ai_assistant_schedule_woocommerce_sync', 468 20, 469 1 470 ); 471 add_action( 472 'woocommerce_product_set_stock_status', 473 'noahfirst_ai_assistant_schedule_woocommerce_sync', 474 20, 475 1 476 ); 477 add_action( 478 'woocommerce_variation_set_stock_status', 479 'noahfirst_ai_assistant_schedule_woocommerce_sync', 480 20, 481 1 482 ); 483 484 function noahfirst_ai_assistant_maybe_schedule_sync($post_id) { 485 if ( 486 empty($post_id) || 487 wp_is_post_revision($post_id) || 488 wp_is_post_autosave($post_id) 489 ) { 490 return; 491 } 492 493 $post_type = get_post_type($post_id); 494 495 if (!in_array($post_type, array('product', 'product_variation', 'post'), true)) { 496 return; 497 } 498 499 noahfirst_ai_assistant_schedule_sync(); 500 } 501 502 add_action( 503 'save_post', 504 'noahfirst_ai_assistant_maybe_schedule_sync', 505 30, 506 1 507 ); 508 509 add_action('trashed_post', function ($post_id) { 510 noahfirst_ai_assistant_schedule_sync(); 511 }, 30, 1); 512 513 add_action('untrashed_post', function ($post_id) { 514 noahfirst_ai_assistant_schedule_sync(); 515 }, 30, 1); 516 517 add_action('before_delete_post', function ($post_id) { 518 noahfirst_ai_assistant_schedule_sync(); 519 }, 30, 1); 520 521 add_action('updated_option', function ($option, $old_value, $value) { 522 if ( 523 in_array( 524 $option, 525 array( 526 'noahfirst_ai_assistant_token', 527 'noahfirst_ai_assistant_secret_key', 528 ), 529 true 530 ) && 531 $old_value !== $value 532 ) { 533 noahfirst_ai_assistant_schedule_sync(3); 534 } 535 }, 10, 3); 536 537 // Add NoahFirst Widget script. 15 538 add_action('wp_footer', function () { 16 539 $token = get_option('noahfirst_ai_assistant_token', ''); … … 23 546 'token', 24 547 rawurlencode($token), 25 'https://api.noahfirst.com/assets/scripts/noah/prod.tanyanoah.v0.0.2.js'548 noahfirst_ai_assistant_get_widget_script_url() 26 549 ); 27 550 28 551 wp_enqueue_script( 29 'noahfirst-ai-assistant-script', // handle30 esc_url($script_url), // path31 [], // dependencies552 'noahfirst-ai-assistant-script', 553 esc_url($script_url), 554 array(), 32 555 '0.0.2', 33 true // load in footer556 true 34 557 ); 35 558 }); 36 559 37 38 // Register "Settings" link under plugin name 39 add_filter('plugin_action_links_' . plugin_basename(__FILE__), function ($links) { 40 $settings_link = '<a href="' . esc_url(admin_url('options-general.php?page=noahfirst-ai-assistant-settings')) . '">Settings</a>'; 41 array_unshift($links, $settings_link); // add to beginning 42 return $links; 560 add_filter( 561 'plugin_action_links_' . plugin_basename(__FILE__), 562 function ($links) { 563 $settings_link = 564 '<a href="' . 565 esc_url( 566 admin_url( 567 'options-general.php?page=noahfirst-ai-assistant-settings' 568 ) 569 ) . 570 '">' . 571 esc_html__('Settings', 'noahfirst-ai-assistant') . 572 '</a>'; 573 array_unshift($links, $settings_link); 574 return $links; 575 } 576 ); 577 578 function noahfirst_ai_assistant_base64url_encode($value) { 579 return rtrim(strtr(base64_encode($value), '+/', '-_'), '='); 580 } 581 582 function noahfirst_ai_assistant_base64url_decode($value) { 583 $padding = strlen($value) % 4; 584 if ($padding) { 585 $value .= str_repeat('=', 4 - $padding); 586 } 587 588 return base64_decode(strtr($value, '-_', '+/'), true); 589 } 590 591 function noahfirst_ai_assistant_encrypt_secret($value) { 592 $value = (string) $value; 593 594 if ($value === '' || strpos($value, 'enc:v1:') === 0) { 595 return $value; 596 } 597 598 if (!function_exists('openssl_encrypt')) { 599 return ''; 600 } 601 602 try { 603 $key = hash('sha256', wp_salt('auth'), true); 604 $iv = random_bytes(12); 605 $tag = ''; 606 $ciphertext = openssl_encrypt( 607 $value, 608 'aes-256-gcm', 609 $key, 610 OPENSSL_RAW_DATA, 611 $iv, 612 $tag 613 ); 614 615 if ($ciphertext === false || $tag === '') { 616 return ''; 617 } 618 619 return implode( 620 ':', 621 array( 622 'enc', 623 'v1', 624 noahfirst_ai_assistant_base64url_encode($iv), 625 noahfirst_ai_assistant_base64url_encode($tag), 626 noahfirst_ai_assistant_base64url_encode($ciphertext), 627 ) 628 ); 629 } catch (Exception $e) { 630 return ''; 631 } 632 } 633 634 function noahfirst_ai_assistant_decrypt_secret($value) { 635 $value = (string) $value; 636 637 if ($value === '' || strpos($value, 'enc:v1:') !== 0) { 638 return $value; 639 } 640 641 if (!function_exists('openssl_decrypt')) { 642 return ''; 643 } 644 645 $parts = explode(':', $value); 646 if (count($parts) !== 5 || $parts[0] !== 'enc' || $parts[1] !== 'v1') { 647 return ''; 648 } 649 650 $iv = noahfirst_ai_assistant_base64url_decode($parts[2]); 651 $tag = noahfirst_ai_assistant_base64url_decode($parts[3]); 652 $ciphertext = noahfirst_ai_assistant_base64url_decode($parts[4]); 653 654 if ($iv === false || $tag === false || $ciphertext === false) { 655 return ''; 656 } 657 658 $plaintext = openssl_decrypt( 659 $ciphertext, 660 'aes-256-gcm', 661 hash('sha256', wp_salt('auth'), true), 662 OPENSSL_RAW_DATA, 663 $iv, 664 $tag 665 ); 666 667 return $plaintext === false ? '' : $plaintext; 668 } 669 670 function noahfirst_ai_assistant_get_secret_key() { 671 return noahfirst_ai_assistant_decrypt_secret( 672 get_option('noahfirst_ai_assistant_secret_key', '') 673 ); 674 } 675 676 function noahfirst_ai_assistant_migrate_secret_storage() { 677 $stored = (string) get_option( 678 'noahfirst_ai_assistant_secret_key', 679 '' 680 ); 681 682 if ($stored !== '' && strpos($stored, 'enc:v1:') !== 0) { 683 $encrypted = noahfirst_ai_assistant_encrypt_secret($stored); 684 if ($encrypted !== $stored) { 685 update_option( 686 'noahfirst_ai_assistant_secret_key', 687 $encrypted, 688 false 689 ); 690 } 691 } 692 } 693 add_action('init', 'noahfirst_ai_assistant_migrate_secret_storage'); 694 695 function noahfirst_ai_assistant_sanitize_secret_key($value) { 696 $value = sanitize_text_field($value); 697 698 // Leaving the password field blank keeps the existing encrypted secret. 699 if ($value === '') { 700 return (string) get_option( 701 'noahfirst_ai_assistant_secret_key', 702 '' 703 ); 704 } 705 706 $encrypted = noahfirst_ai_assistant_encrypt_secret($value); 707 708 if ($encrypted === '') { 709 add_settings_error( 710 'noahfirst_ai_assistant_secret_key', 711 'noahfirst_secret_encryption_failed', 712 __( 713 'Integration secret was not saved because secure encryption is unavailable on this WordPress host.', 714 'noahfirst-ai-assistant' 715 ), 716 'error' 717 ); 718 719 return (string) get_option( 720 'noahfirst_ai_assistant_secret_key', 721 '' 722 ); 723 } 724 725 return $encrypted; 726 } 727 728 function noahfirst_ai_assistant_register_settings() { 729 register_setting( 730 'noahfirst_ai_assistant_settings_group', 731 'noahfirst_ai_assistant_token', 732 array( 733 'type' => 'string', 734 'default' => '', 735 'sanitize_callback' => 'sanitize_text_field', 736 ) 737 ); 738 739 register_setting( 740 'noahfirst_ai_assistant_settings_group', 741 'noahfirst_ai_assistant_secret_key', 742 array( 743 'type' => 'string', 744 'default' => '', 745 'sanitize_callback' => 746 'noahfirst_ai_assistant_sanitize_secret_key', 747 ) 748 ); 749 } 750 add_action('admin_init', 'noahfirst_ai_assistant_register_settings'); 751 752 function noahfirst_ai_assistant_set_admin_notice($status) { 753 if (!in_array($status, array('success', 'error'), true)) { 754 return; 755 } 756 757 set_transient( 758 'noahfirst_ai_assistant_admin_notice', 759 $status, 760 60 761 ); 762 } 763 764 function noahfirst_ai_assistant_get_and_clear_admin_notice() { 765 $status = get_transient('noahfirst_ai_assistant_admin_notice'); 766 767 if ($status !== 'success' && $status !== 'error') { 768 return ''; 769 } 770 771 delete_transient('noahfirst_ai_assistant_admin_notice'); 772 773 return $status; 774 } 775 776 add_action( 777 'admin_post_noahfirst_ai_assistant_resync_products', 778 function () { 779 if (!current_user_can('manage_options')) { 780 wp_die( 781 esc_html__( 782 'You do not have permission to do that.', 783 'noahfirst-ai-assistant' 784 ) 785 ); 786 } 787 788 check_admin_referer( 789 'noahfirst_ai_assistant_resync_products' 790 ); 791 792 $result = noahfirst_ai_assistant_sync_store_to_api(); 793 noahfirst_ai_assistant_set_admin_notice( 794 $result ? 'success' : 'error' 795 ); 796 797 $redirect = add_query_arg( 798 array( 799 'page' => 'noahfirst-ai-assistant-settings', 800 ), 801 admin_url('options-general.php') 802 ); 803 804 wp_safe_redirect($redirect); 805 exit; 806 } 807 ); 808 809 add_action('admin_notices', function () { 810 $screen = function_exists('get_current_screen') 811 ? get_current_screen() 812 : null; 813 814 if (!$screen || $screen->id !== 'settings_page_noahfirst-ai-assistant-settings') { 815 return; 816 } 817 818 $status = noahfirst_ai_assistant_get_and_clear_admin_notice(); 819 820 if ($status === 'success') { 821 echo '<div class="notice notice-success is-dismissible"><p>' . 822 esc_html__( 823 'Store knowledge synced successfully.', 824 'noahfirst-ai-assistant' 825 ) . 826 '</p></div>'; 827 return; 828 } 829 830 if ($status === 'error') { 831 echo '<div class="notice notice-error is-dismissible"><p>' . 832 esc_html__( 833 'Store sync failed. The last known good catalog remains active in NoahFirst.', 834 'noahfirst-ai-assistant' 835 ) . 836 '</p></div>'; 837 } 43 838 }); 44 839 45 46 // Register the setting fields during admin_init 47 function noahfirst_ai_assistant_register_settings() 48 { 49 register_setting('noahfirst_ai_assistant_settings_group', 'noahfirst_ai_assistant_token', array( 50 'type' => 'string', 51 'default' => '', 52 'sanitize_callback' => 'sanitize_text_field' 53 )); 54 } 55 add_action('admin_init', 'noahfirst_ai_assistant_register_settings'); 56 57 58 // Register settings page 59 function noahfirst_ai_assistant_register_settings_page() 60 { 840 function noahfirst_ai_assistant_register_settings_page() { 61 841 add_options_page( 62 842 'NoahFirst AI Assistant', … … 67 847 ); 68 848 } 69 add_action('admin_menu', 'noahfirst_ai_assistant_register_settings_page'); 70 71 72 // Render settings page 73 function noahfirst_ai_assistant_render_settings_page() 74 { 849 add_action( 850 'admin_menu', 851 'noahfirst_ai_assistant_register_settings_page' 852 ); 853 854 function noahfirst_ai_assistant_render_settings_page() { 855 if (!current_user_can('manage_options')) { 856 return; 857 } 858 859 $token = get_option('noahfirst_ai_assistant_token', ''); 860 $has_secret = 861 get_option('noahfirst_ai_assistant_secret_key', '') !== ''; 862 $last_sync = get_option( 863 'noahfirst_ai_assistant_last_sync_at', 864 '' 865 ); 866 $last_status = get_option( 867 'noahfirst_ai_assistant_last_sync_status', 868 '' 869 ); 75 870 ?> 76 <div style="padding:32px ">77 <img width="256 px" src="<?php echo esc_url(plugin_dir_url(__FILE__)) . 'noahfirst-logo.png' ?>" />78 79 < !-- Mandatory Settings -->80 < h2 style="margin-top:32px"><?php esc_html_e('Welcome! Let\'s connect your account', 'noahfirst-ai-assistant'); ?></h2>81 <div>82 <?php esc_html_e('Obtain the token from your account on noahfirst.com. Go to ', 'noahfirst-ai-assistant'); ?><a href="https://noahfirst.com/panel"><?php esc_html_e('your panel', 'noahfirst-ai-assistant'); ?></a><?php esc_html_e(" > NoahFirst AI Assistant > General Settings. If you don't have an account,", 'noahfirst-ai-assistant'); ?> <a href="https://noahfirst.com/sign-up"><?php esc_html_e("register here", 'noahfirst-ai-assistant'); ?></a>.83 </div> 871 <div style="padding:32px;max-width:960px"> 872 <img width="256" src="<?php echo esc_url(plugin_dir_url(__FILE__) . 'noahfirst-logo.png'); ?>" alt="NoahFirst" /> 873 874 <h2 style="margin-top:32px"><?php esc_html_e('Connect this store to NoahFirst', 'noahfirst-ai-assistant'); ?></h2> 875 <p> 876 <?php esc_html_e('Copy the Store Token and Integration Secret from NoahFirst → Store, paste them here, then save. Products and WordPress posts will sync automatically.', 'noahfirst-ai-assistant'); ?> 877 </p> 878 84 879 <form method="post" action="options.php"> 85 880 <?php 86 if (!current_user_can('manage_options')) {87 return;88 }89 90 881 settings_fields('noahfirst_ai_assistant_settings_group'); 91 882 do_settings_sections('noahfirst_ai_assistant_settings_group'); 92 $token = get_option('noahfirst_ai_assistant_token', '');93 883 ?> 94 884 <table class="form-table" role="presentation"> 95 885 <tr> 96 886 <th scope="row"> 97 <label for="noahfirst_ai_assistant_token"><?php esc_html_e(' Token', 'noahfirst-ai-assistant'); ?></label>887 <label for="noahfirst_ai_assistant_token"><?php esc_html_e('Store token', 'noahfirst-ai-assistant'); ?></label> 98 888 </th> 99 889 <td> 100 <input name="noahfirst_ai_assistant_token" type="text" id="noahfirst_ai_assistant_token" value="<?php echo esc_attr($token); ?>" /> 101 <p class="description"><?php esc_html_e('The token from your NoahFirst account.', 'noahfirst-ai-assistant'); ?></p> 890 <input name="noahfirst_ai_assistant_token" type="text" id="noahfirst_ai_assistant_token" value="<?php echo esc_attr($token); ?>" class="regular-text code" autocomplete="off" /> 891 <p class="description"><?php esc_html_e('Identifies this NoahFirst store. It is not the integration secret.', 'noahfirst-ai-assistant'); ?></p> 892 </td> 893 </tr> 894 <tr> 895 <th scope="row"> 896 <label for="noahfirst_ai_assistant_secret_key"><?php esc_html_e('Integration secret', 'noahfirst-ai-assistant'); ?></label> 897 </th> 898 <td> 899 <input name="noahfirst_ai_assistant_secret_key" type="password" id="noahfirst_ai_assistant_secret_key" value="" class="regular-text code" autocomplete="new-password" placeholder="<?php echo esc_attr($has_secret ? __('Configured — leave blank to keep', 'noahfirst-ai-assistant') : __('Paste secret from NoahFirst', 'noahfirst-ai-assistant')); ?>" /> 900 <p class="description"><?php esc_html_e('Stored only in WordPress and validated against a one-way hash in NoahFirst.', 'noahfirst-ai-assistant'); ?></p> 102 901 </td> 103 902 </tr> 104 903 </table> 105 <?php submit_button( ); ?>904 <?php submit_button(__('Save & connect', 'noahfirst-ai-assistant')); ?> 106 905 </form> 107 <!-- End of Mandatory Settings --> 906 907 <hr style="margin:32px 0" /> 908 909 <h2><?php esc_html_e('Store sync', 'noahfirst-ai-assistant'); ?></h2> 910 <p> 911 <?php 912 if ($last_sync) { 913 echo esc_html( 914 sprintf( 915 /* translators: last attempt of products sync */ 916 __('Last attempt: %1$s · %2$s', 'noahfirst-ai-assistant'), 917 $last_sync, 918 $last_status ?: 'unknown' 919 ) 920 ); 921 } else { 922 esc_html_e('No sync has run yet.', 'noahfirst-ai-assistant'); 923 } 924 ?> 925 </p> 926 927 <form method="post" action="<?php echo esc_url(admin_url('admin-post.php')); ?>"> 928 <?php wp_nonce_field('noahfirst_ai_assistant_resync_products'); ?> 929 <input type="hidden" name="action" value="noahfirst_ai_assistant_resync_products" /> 930 <button type="submit" class="button button-secondary"> 931 <?php esc_html_e('Sync now', 'noahfirst-ai-assistant'); ?> 932 </button> 933 </form> 108 934 </div> 109 935 <?php -
noahfirst-ai-assistant/tags/1.1.0/readme.txt
r3536497 r3737499 1 === NoahFirst AI Assistant===1 === NoahFirst AI Sales Associate === 2 2 Contributors: noahfirstai, noahfirst 3 Tags: Chatbot, AI Agent, live chat, woocommerce, ecommerce3 Tags: woocommerce, ecommerce, ai sales associate, customer support, live chat 4 4 Requires at least: 5.0 5 5 Requires PHP: 7.1 6 Tested up to: 6.97 Stable tag: 1. 0.06 Tested up to: 7.1 7 Stable tag: 1.1.0 8 8 License: GPLv2 or later 9 9 License URI: https://www.gnu.org/licenses/gpl-2.0.html 10 Boost your ecommerce sales with an AI chatbot that understands your store data and turns conversations into customers.10 AI sales associate for online stores that answers buyer questions using your own store data. 11 11 12 12 == Description == 13 13 14 Try NoahFirst free for 30 days and give your store a 24/7 AI assistant that supports 15 customers instantly and helps turn visitors into buyers. 14 NoahFirst is an AI sales associate for independent online stores. It uses your own 15 store knowledge—products, inventory, product details, and published WordPress 16 content—to answer buyer questions while they decide. 16 17 17 NoahFirst is an AI assistant designed for e-commerce stores running on WordPress. 18 Unlike generic bots that rely on external data or guesswork, NoahFirst uses your store’s 19 own data—products, inventory, and policies—to deliver accurate, on-brand 20 answers. It’s like having a digital version of your best store employee—one who 21 knows your catalog inside and out, know your customer the best 18 Unlike a generic chatbot, NoahFirst is designed around ecommerce buying questions. 19 The official WordPress plugin connects the store to NoahFirst, loads the storefront 20 assistant, and keeps structured WooCommerce product knowledge synchronized. 22 21 23 22 == Why NoahFirst? == 24 23 25 NoahFirst works differently from traditional chat tools. Instead of relying on 26 scripts or external data, it uses your store’s own products, inventory, and 27 policies as its source of truth—so every answer stays accurate, consistent, 28 and aligned with your brand. 24 NoahFirst works from your store data rather than guessing from unrelated sources. 25 Structured WooCommerce product facts such as price, stock status, SKU, variants, 26 attributes, and canonical product URLs remain authoritative for shopper answers. 29 27 30 Because NoahFirst operates directly on your store data, it can respond instantly 31 without waiting for human input, giving customers the clarity they need to 32 make decisions without delay. 28 Customers ask. NoahFirst answers. Sales happen. 33 29 34 30 == Key Features == 35 31 36 * Content-Bound Intelligence: Uses your WooCommerce store data (products, inventory, policies) as the primary knowledge source 37 * Real-Time Data Sync: Automatically reflects updates in stock, pricing, and product details without manual retraining 38 * Context-Aware Response Engine: Understands customer intent and generates relevant answers based on your store data 39 * Multi-Language Support: Responds to customers in multiple languages based on their input 40 * Ecommerce-Native Integration: Connects directly with your store structure for accurate and consistent responses 41 * Lightweight Script Loading: Runs asynchronously without blocking your site performance 42 * No-Code Setup: Install and connect your store without manual training or configuration 32 * Store-grounded answers: Uses WooCommerce products and published WordPress content as store knowledge 33 * Automatic store sync: Product, inventory, variation, pricing, and post changes schedule a NoahFirst sync 34 * Scheduled safety sync: A twice-daily heartbeat refreshes connected store knowledge 35 * Manual resync: Store administrators can trigger a sync and see the latest sync status 36 * Structured commerce ingestion: Sends product facts directly to NoahFirst without WooCommerce REST consumer credentials 37 * Last-known-good protection: NoahFirst keeps the prior successful catalog if an ingestion attempt fails 38 * Secure connection settings: The Integration Secret is encrypted at rest inside WordPress 39 * Multi-language shopper responses: NoahFirst can respond based on the shopper's language 40 * Lightweight storefront loader: The assistant script loads on connected storefronts 41 * No-code setup: Install the plugin, connect the store, and let NoahFirst handle ongoing sync 43 42 44 43 == How It Works == 45 44 46 45 1. Install and activate the NoahFirst plugin 47 2. Connect your NoahFirst account 48 3. NoahFirst syncs with your WooCommerce store data 49 4. Your AI assistant is ready to answer customers instantly 46 2. Copy the Store Token and Integration Secret from NoahFirst → Store 47 3. Save the connection in WordPress 48 4. NoahFirst performs the initial store sync 49 5. Product, stock, variation, and post changes schedule future syncs automatically 50 6. The storefront assistant can answer shoppers using the connected store knowledge 50 51 51 52 == Use Cases == 52 53 53 * When customers ask about product details, NoahFirst answers instantly using your store data54 * When shoppers check sizing, availability, or delivery, NoahFirst provides accurate responses in real time55 * When your team is unavailable, NoahFirst continues supporting customers 24/756 * When visitors hesitate before buying, NoahFirst answers their questions and helps them move forward57 * During high traffic or campaigns, NoahFirst handles multiple conversations at once without delay54 * Product-detail and product-selection questions 55 * Size, compatibility, availability, and usage questions 56 * Product comparison and catalog guidance 57 * Store-policy questions supported by published store knowledge 58 * Shopper questions when the merchant team is unavailable 58 59 59 60 == Frequently Asked Questions == 60 61 61 62 = Does NoahFirst require WooCommerce? = 62 NoahFirst is optimized for WooCommerce stores but can also work on general WordPress websites. 63 NoahFirst is optimized for WooCommerce stores. The plugin can also sync published 64 WordPress posts as supporting store knowledge. 63 65 64 66 = Does NoahFirst use my store data? = 65 Yes. NoahFirst uses your store’s own data to provide accurate andrelevant answers.67 Yes. NoahFirst uses connected store knowledge to provide relevant answers. 66 68 67 = Does NoahFirst slow down my website? = 68 No. NoahFirst is lightweight and designed to run without affecting your site performance. 69 = How does automatic sync work? = 70 Connected stores schedule a sync when WooCommerce products, variations, stock, or 71 published WordPress posts change. A scheduled heartbeat also refreshes the store 72 periodically. Administrators can trigger a manual sync from the plugin settings page. 73 74 = Does NoahFirst use WooCommerce REST API consumer keys? = 75 No. The official plugin pushes structured store data to NoahFirst using the Store 76 Token and Integration Secret configured by the administrator. 69 77 70 78 = Is technical setup required? = 71 No. NoahFirst is designed to work with minimal setup and no coding required. 72 73 = Will the AI sound like a robot? = 74 No. NoahFirst adapts to your brand voice. Whether your store is professional, playful, or premium, it responds in a way that feels natural and aligned with your brand. 75 76 = Does the trial require a credit card? = 77 No. You can start your 30-day free trial with no credit card required. 79 No coding is required. A store administrator installs the plugin and connects the 80 credentials supplied by NoahFirst. 78 81 79 82 == External Services == 80 83 81 This plugin connects to the NoahFirst API (https://api.noahfirst.com) to provide AI-powered responses within your store. 84 This plugin connects to the NoahFirst API (https://api.noahfirst.com) and loads the 85 NoahFirst storefront assistant script from NoahFirst infrastructure. 82 86 83 * Service: NoahFirst API 84 * Purpose: Processes customer queries and store data to generate real-time responses. 85 * Data Sent: Customer messages entered in the chat widget and relevant store data (such as product details, inventory, and store policies). 86 * When Data is Sent: Data is transmitted only when a user submits a query through the chat interface or when the assistant is triggered to generate a response. 87 * Subprocessors: This service relies on OpenAI (AI processing provider) and IDCloudHost (infrastructure provider). Data is processed solely to generate real-time responses and is not used to train shared or public AI models. All subprocessors are bound by data processing agreements requiring confidentiality and secure handling of data. 88 * Security: All data is transmitted securely using HTTPS (TLS encryption). Data at rest is protected using infrastructure-level security controls. 87 Service: NoahFirst API 88 Purpose: Authenticates the connected store, receives structured store knowledge, 89 and powers shopper-facing NoahFirst responses. 90 Data Sent During Store Sync: Store Token, Integration Secret for request 91 authentication, site URL, WooCommerce product and variation data (including names, 92 descriptions, prices, stock, SKUs, attributes, categories, and product URLs), and 93 published WordPress post content selected by the plugin. 94 When Store Data Is Sent: After connection settings are saved, when relevant 95 WooCommerce product/stock/variation or WordPress post changes schedule a sync, 96 during the scheduled store-sync heartbeat, or when an administrator chooses 97 "Sync now". 98 Shopper Data: When a shopper uses the NoahFirst storefront assistant, the widget 99 communicates with the NoahFirst service to process that shopper conversation. 100 Subprocessors: NoahFirst uses OpenAI for AI processing and IDCloudHost for 101 infrastructure hosting. 102 Security: Data is transmitted over HTTPS. The WordPress Integration Secret is 103 stored encrypted by the plugin using the WordPress installation's authentication salt. 89 104 90 105 Policies: 91 *Privacy Policy: https://noahfirst.com/privacy92 *Terms of Service: https://noahfirst.com/terms93 *Security: https://noahfirst.com/security106 Privacy Policy: https://noahfirst.com/privacy 107 Terms of Service: https://noahfirst.com/terms 108 Security: https://noahfirst.com/security 94 109 95 110 == Screenshots == 96 111 97 1. NoahFirst can answer a product based on its information 98 2. NoahFirst can answer about delivery information 99 3. NoahFirst can recommend a product based on customer's needs 100 4. NoahFirst can answer payment information 101 5. NoahFirst can recommend a product based on customer's needs 102 6. NoahFirst can recommend a product based on customer's needs 103 7. NoahFirst understands your product 104 8. NoahFirst can recommend a product based on customer's needs 112 1. NoahFirst answers product questions using connected store information 113 2. NoahFirst supports delivery and policy questions when the store provides that knowledge 114 3. NoahFirst helps shoppers compare and select products 115 4. NoahFirst can answer availability and product-detail questions 105 116 106 117 == Changelog == 118 119 = 1.1.0 = 120 121 * Added structured WooCommerce product and variation ingestion 122 * Added automatic sync hooks for product, stock, variation, and WordPress post changes 123 * Added twice-daily sync heartbeat and manual "Sync now" control 124 * Added sync status visibility for store administrators 125 * Added encrypted storage for the Integration Secret 126 * Added last-known-good catalog protection through the NoahFirst ingestion contract 127 * Added real WordPress and WooCommerce runtime verification in CI 107 128 108 129 = 1.0.0 = … … 112 133 == Upgrade Notice == 113 134 114 = 1.0.0 = 115 Initial release 135 = 1.1.0 = 136 Adds automatic structured store sync and secure connection storage for the official 137 NoahFirst WordPress integration. -
noahfirst-ai-assistant/trunk/noahfirst-ai-assistant.php
r3534632 r3737499 2 2 3 3 /** 4 * Plugin Name: NoahFirst AI Assistant5 * Description: Boost your ecommerce sales with an AI chatbot that understands your store data and turns conversations into customers.6 * Version: 1. 0.04 * Plugin Name: NoahFirst AI Sales Associate 5 * Description: AI sales associate for online stores that answers buyer questions using your own store data. 6 * Version: 1.1.0 7 7 * License: GPLv2 or later 8 8 * Author: NoahFirstAI … … 12 12 defined('ABSPATH') || exit; 13 13 14 // Add NoahFirst Widget script 14 const NOAHFIRST_AI_ASSISTANT_SYNC_HOOK = 'noahfirst_ai_assistant_run_store_sync'; 15 16 function noahfirst_ai_assistant_get_api_url() { 17 return apply_filters( 18 'noahfirst_ai_assistant_api_url', 19 'https://api.noahfirst.com/graphql' 20 ); 21 } 22 23 function noahfirst_ai_assistant_get_widget_script_url() { 24 return apply_filters( 25 'noahfirst_ai_assistant_widget_script_url', 26 'https://api.noahfirst.com/assets/scripts/noah/prod.tanyanoah.v0.0.2.js' 27 ); 28 } 29 30 function noahfirst_ai_assistant_graphql_request($query, $variables, $secret_key, $operation_name) { 31 $response = wp_remote_post( 32 noahfirst_ai_assistant_get_api_url(), 33 array( 34 'method' => 'POST', 35 'timeout' => 60, 36 'redirection' => 3, 37 'headers' => array( 38 'Content-Type' => 'application/json', 39 'x-noahfirst-secret-key' => $secret_key, 40 'x-apollo-operation-name' => $operation_name, 41 'apollo-require-preflight' => 'true', 42 ), 43 'body' => wp_json_encode( 44 array( 45 'query' => $query, 46 'variables' => $variables, 47 ) 48 ), 49 ) 50 ); 51 52 if (is_wp_error($response)) { 53 return false; 54 } 55 56 $status_code = wp_remote_retrieve_response_code($response); 57 $body = wp_remote_retrieve_body($response); 58 $decoded = json_decode($body, true); 59 60 if ( 61 $status_code < 200 || 62 $status_code >= 300 || 63 !is_array($decoded) || 64 !empty($decoded['errors']) 65 ) { 66 return false; 67 } 68 69 return $decoded; 70 } 71 72 function noahfirst_ai_assistant_format_product_attributes($product) { 73 $formatted = array(); 74 75 if (!$product instanceof WC_Product) { 76 return $formatted; 77 } 78 79 foreach ($product->get_attributes() as $key => $attribute) { 80 if ($attribute instanceof WC_Product_Attribute) { 81 $name = $attribute->get_name(); 82 $values = array(); 83 84 if ($attribute->is_taxonomy()) { 85 $terms = wc_get_product_terms( 86 $product->get_id(), 87 $name, 88 array('fields' => 'names') 89 ); 90 if (!is_wp_error($terms)) { 91 $values = array_values($terms); 92 } 93 } else { 94 $values = array_values($attribute->get_options()); 95 } 96 97 $formatted[$name] = $values; 98 continue; 99 } 100 101 if (is_array($attribute)) { 102 $formatted[(string) $key] = array_values($attribute); 103 continue; 104 } 105 106 $formatted[(string) $key] = array((string) $attribute); 107 } 108 109 return $formatted; 110 } 111 112 function noahfirst_ai_assistant_format_variation_attributes($variation) { 113 $formatted = array(); 114 115 if (!$variation instanceof WC_Product_Variation) { 116 return $formatted; 117 } 118 119 foreach ($variation->get_attributes() as $key => $value) { 120 $formatted[(string) $key] = array((string) $value); 121 } 122 123 return $formatted; 124 } 125 126 function noahfirst_ai_assistant_get_product_variations($product) { 127 $variations = array(); 128 129 if ( 130 !$product instanceof WC_Product_Variable || 131 !method_exists($product, 'get_children') 132 ) { 133 return $variations; 134 } 135 136 foreach ($product->get_children() as $variation_id) { 137 $variation = wc_get_product($variation_id); 138 139 if ( 140 !$variation instanceof WC_Product_Variation || 141 $variation->get_status() !== 'publish' 142 ) { 143 continue; 144 } 145 146 $modified = $variation->get_date_modified(); 147 148 $variations[] = array( 149 'id' => $variation->get_id(), 150 'sku' => $variation->get_sku(), 151 'status' => $variation->get_status(), 152 'price' => $variation->get_price(), 153 'regular_price' => $variation->get_regular_price(), 154 'sale_price' => $variation->get_sale_price(), 155 'stock_status' => $variation->get_stock_status(), 156 'stock_quantity' => $variation->get_stock_quantity(), 157 'manage_stock' => $variation->get_manage_stock(), 158 'backorders' => $variation->get_backorders(), 159 'attributes' => noahfirst_ai_assistant_format_variation_attributes($variation), 160 'permalink' => get_permalink($product->get_id()), 161 'date_modified' => $modified ? $modified->date('c') : null, 162 ); 163 } 164 165 return $variations; 166 } 167 168 function noahfirst_ai_assistant_get_products_payload() { 169 $products = array(); 170 171 if (!class_exists('WooCommerce')) { 172 return $products; 173 } 174 175 $product_query = new WC_Product_Query( 176 array( 177 'limit' => -1, 178 'status' => 'publish', 179 'orderby' => 'title', 180 'order' => 'ASC', 181 ) 182 ); 183 184 $items = $product_query->get_products(); 185 $currency = get_woocommerce_currency(); 186 187 foreach ($items as $product) { 188 if (!$product instanceof WC_Product) { 189 continue; 190 } 191 192 $category_names = array(); 193 $terms = get_the_terms($product->get_id(), 'product_cat'); 194 195 if (!empty($terms) && !is_wp_error($terms)) { 196 foreach ($terms as $term) { 197 $category_names[] = $term->name; 198 } 199 } 200 201 $modified = $product->get_date_modified(); 202 203 $products[] = array( 204 'id' => $product->get_id(), 205 'name' => $product->get_name(), 206 'slug' => $product->get_slug(), 207 'sku' => $product->get_sku(), 208 'type' => $product->get_type(), 209 'status' => $product->get_status(), 210 'currency' => $currency, 211 'price' => $product->get_price(), 212 'regular_price' => $product->get_regular_price(), 213 'sale_price' => $product->get_sale_price(), 214 'stock_status' => $product->get_stock_status(), 215 'stock_quantity' => $product->get_stock_quantity(), 216 'manage_stock' => $product->get_manage_stock(), 217 'backorders' => $product->get_backorders(), 218 'description' => wp_strip_all_tags($product->get_description()), 219 'short_description' => wp_strip_all_tags($product->get_short_description()), 220 'permalink' => get_permalink($product->get_id()), 221 'categories' => $category_names, 222 'attributes' => noahfirst_ai_assistant_format_product_attributes($product), 223 'variations' => noahfirst_ai_assistant_get_product_variations($product), 224 'date_modified' => $modified ? $modified->date('c') : null, 225 ); 226 } 227 228 return $products; 229 } 230 231 function noahfirst_ai_assistant_get_posts_payload() { 232 $posts = get_posts( 233 array( 234 'post_type' => 'post', 235 'post_status' => 'publish', 236 'posts_per_page' => -1, 237 'orderby' => 'title', 238 'order' => 'ASC', 239 'suppress_filters' => false, 240 ) 241 ); 242 243 $payload = array(); 244 245 foreach ($posts as $post) { 246 $payload[] = array( 247 'id' => $post->ID, 248 'name' => $post->post_title, 249 'slug' => $post->post_name, 250 'type' => $post->post_type, 251 'status' => $post->post_status, 252 'description' => wp_strip_all_tags($post->post_content), 253 'short_description' => wp_strip_all_tags($post->post_excerpt), 254 'permalink' => get_permalink($post->ID), 255 'categories' => wp_get_post_categories( 256 $post->ID, 257 array('fields' => 'names') 258 ), 259 'date_modified' => get_post_modified_time('c', true, $post), 260 ); 261 } 262 263 return $payload; 264 } 265 266 function noahfirst_ai_assistant_push_catalog($token, $secret_key, $products) { 267 $query = <<<'GRAPHQL' 268 mutation IngestCommerceCatalog($input: CommerceCatalogIngestionInput!) { 269 ingestCommerceCatalogByToken(input: $input) { 270 accepted 271 status 272 product_count 273 variant_count 274 last_success_at 275 has_last_good 276 } 277 } 278 GRAPHQL; 279 280 $catalog = array( 281 'schema_version' => 1, 282 'source' => 'wordpress', 283 'generated_at' => gmdate('c'), 284 'products' => $products, 285 ); 286 287 return noahfirst_ai_assistant_graphql_request( 288 $query, 289 array( 290 'input' => array( 291 'token' => (string) $token, 292 'platform' => 'wordpress', 293 'site_url' => home_url('/'), 294 'generated_at' => gmdate('c'), 295 'catalog_json' => wp_json_encode($catalog), 296 ), 297 ), 298 $secret_key, 299 'IngestCommerceCatalog' 300 ); 301 } 302 303 function noahfirst_ai_assistant_push_posts($token, $secret_key, $posts) { 304 $query = <<<'GRAPHQL' 305 mutation IngestPlatformContent($input: PlatformContentIngestionInput!) { 306 ingestPlatformContentByToken(input: $input) { 307 accepted 308 content_type 309 item_count 310 } 311 } 312 GRAPHQL; 313 314 return noahfirst_ai_assistant_graphql_request( 315 $query, 316 array( 317 'input' => array( 318 'token' => (string) $token, 319 'platform' => 'wordpress', 320 'site_url' => home_url('/'), 321 'content_type' => 'posts', 322 'content_json' => wp_json_encode( 323 array( 324 'schema_version' => 1, 325 'source' => 'wordpress', 326 'generated_at' => gmdate('c'), 327 'posts' => $posts, 328 ) 329 ), 330 ), 331 ), 332 $secret_key, 333 'IngestPlatformContent' 334 ); 335 } 336 337 function noahfirst_ai_assistant_sync_store_to_api() { 338 $token = trim( 339 (string) get_option('noahfirst_ai_assistant_token', '') 340 ); 341 $secret_key = trim( 342 (string) noahfirst_ai_assistant_get_secret_key() 343 ); 344 345 if (empty($token) || empty($secret_key)) { 346 return false; 347 } 348 349 // Empty payloads are intentionally sent so a store that deletes its last 350 // product/post does not leave stale knowledge behind in NoahFirst. 351 $products_ok = (bool) noahfirst_ai_assistant_push_catalog( 352 $token, 353 $secret_key, 354 noahfirst_ai_assistant_get_products_payload() 355 ); 356 357 $posts_ok = (bool) noahfirst_ai_assistant_push_posts( 358 $token, 359 $secret_key, 360 noahfirst_ai_assistant_get_posts_payload() 361 ); 362 363 $success = $products_ok && $posts_ok; 364 365 update_option( 366 'noahfirst_ai_assistant_last_sync_at', 367 gmdate('c'), 368 false 369 ); 370 update_option( 371 'noahfirst_ai_assistant_last_sync_status', 372 $success ? 'success' : 'error', 373 false 374 ); 375 376 return $success; 377 } 378 379 function noahfirst_ai_assistant_schedule_sync($delay = 20) { 380 if ( 381 !get_option('noahfirst_ai_assistant_token', '') || 382 !get_option('noahfirst_ai_assistant_secret_key', '') 383 ) { 384 return; 385 } 386 387 if (!wp_next_scheduled(NOAHFIRST_AI_ASSISTANT_SYNC_HOOK)) { 388 wp_schedule_single_event( 389 time() + max(1, (int) $delay), 390 NOAHFIRST_AI_ASSISTANT_SYNC_HOOK 391 ); 392 } 393 } 394 395 add_action( 396 NOAHFIRST_AI_ASSISTANT_SYNC_HOOK, 397 'noahfirst_ai_assistant_sync_store_to_api' 398 ); 399 400 const NOAHFIRST_AI_ASSISTANT_HEARTBEAT_HOOK = 'noahfirst_ai_assistant_store_sync_heartbeat'; 401 402 function noahfirst_ai_assistant_register_heartbeat() { 403 if (!wp_next_scheduled(NOAHFIRST_AI_ASSISTANT_HEARTBEAT_HOOK)) { 404 wp_schedule_event( 405 time() + HOUR_IN_SECONDS, 406 'twicedaily', 407 NOAHFIRST_AI_ASSISTANT_HEARTBEAT_HOOK 408 ); 409 } 410 } 411 412 function noahfirst_ai_assistant_clear_heartbeat() { 413 $timestamp = wp_next_scheduled(NOAHFIRST_AI_ASSISTANT_HEARTBEAT_HOOK); 414 415 while ($timestamp) { 416 wp_unschedule_event( 417 $timestamp, 418 NOAHFIRST_AI_ASSISTANT_HEARTBEAT_HOOK 419 ); 420 $timestamp = wp_next_scheduled(NOAHFIRST_AI_ASSISTANT_HEARTBEAT_HOOK); 421 } 422 } 423 424 register_activation_hook( 425 __FILE__, 426 'noahfirst_ai_assistant_register_heartbeat' 427 ); 428 register_deactivation_hook( 429 __FILE__, 430 'noahfirst_ai_assistant_clear_heartbeat' 431 ); 432 433 add_action( 434 'init', 435 'noahfirst_ai_assistant_register_heartbeat' 436 ); 437 438 add_action( 439 NOAHFIRST_AI_ASSISTANT_HEARTBEAT_HOOK, 440 'noahfirst_ai_assistant_sync_store_to_api' 441 ); 442 443 function noahfirst_ai_assistant_schedule_woocommerce_sync() { 444 noahfirst_ai_assistant_schedule_sync(5); 445 } 446 447 add_action( 448 'woocommerce_update_product', 449 'noahfirst_ai_assistant_schedule_woocommerce_sync', 450 20, 451 1 452 ); 453 add_action( 454 'woocommerce_new_product', 455 'noahfirst_ai_assistant_schedule_woocommerce_sync', 456 20, 457 1 458 ); 459 add_action( 460 'woocommerce_product_set_stock', 461 'noahfirst_ai_assistant_schedule_woocommerce_sync', 462 20, 463 1 464 ); 465 add_action( 466 'woocommerce_variation_set_stock', 467 'noahfirst_ai_assistant_schedule_woocommerce_sync', 468 20, 469 1 470 ); 471 add_action( 472 'woocommerce_product_set_stock_status', 473 'noahfirst_ai_assistant_schedule_woocommerce_sync', 474 20, 475 1 476 ); 477 add_action( 478 'woocommerce_variation_set_stock_status', 479 'noahfirst_ai_assistant_schedule_woocommerce_sync', 480 20, 481 1 482 ); 483 484 function noahfirst_ai_assistant_maybe_schedule_sync($post_id) { 485 if ( 486 empty($post_id) || 487 wp_is_post_revision($post_id) || 488 wp_is_post_autosave($post_id) 489 ) { 490 return; 491 } 492 493 $post_type = get_post_type($post_id); 494 495 if (!in_array($post_type, array('product', 'product_variation', 'post'), true)) { 496 return; 497 } 498 499 noahfirst_ai_assistant_schedule_sync(); 500 } 501 502 add_action( 503 'save_post', 504 'noahfirst_ai_assistant_maybe_schedule_sync', 505 30, 506 1 507 ); 508 509 add_action('trashed_post', function ($post_id) { 510 noahfirst_ai_assistant_schedule_sync(); 511 }, 30, 1); 512 513 add_action('untrashed_post', function ($post_id) { 514 noahfirst_ai_assistant_schedule_sync(); 515 }, 30, 1); 516 517 add_action('before_delete_post', function ($post_id) { 518 noahfirst_ai_assistant_schedule_sync(); 519 }, 30, 1); 520 521 add_action('updated_option', function ($option, $old_value, $value) { 522 if ( 523 in_array( 524 $option, 525 array( 526 'noahfirst_ai_assistant_token', 527 'noahfirst_ai_assistant_secret_key', 528 ), 529 true 530 ) && 531 $old_value !== $value 532 ) { 533 noahfirst_ai_assistant_schedule_sync(3); 534 } 535 }, 10, 3); 536 537 // Add NoahFirst Widget script. 15 538 add_action('wp_footer', function () { 16 539 $token = get_option('noahfirst_ai_assistant_token', ''); … … 23 546 'token', 24 547 rawurlencode($token), 25 'https://api.noahfirst.com/assets/scripts/noah/prod.tanyanoah.v0.0.2.js'548 noahfirst_ai_assistant_get_widget_script_url() 26 549 ); 27 550 28 551 wp_enqueue_script( 29 'noahfirst-ai-assistant-script', // handle30 esc_url($script_url), // path31 [], // dependencies552 'noahfirst-ai-assistant-script', 553 esc_url($script_url), 554 array(), 32 555 '0.0.2', 33 true // load in footer556 true 34 557 ); 35 558 }); 36 559 37 38 // Register "Settings" link under plugin name 39 add_filter('plugin_action_links_' . plugin_basename(__FILE__), function ($links) { 40 $settings_link = '<a href="' . esc_url(admin_url('options-general.php?page=noahfirst-ai-assistant-settings')) . '">Settings</a>'; 41 array_unshift($links, $settings_link); // add to beginning 42 return $links; 560 add_filter( 561 'plugin_action_links_' . plugin_basename(__FILE__), 562 function ($links) { 563 $settings_link = 564 '<a href="' . 565 esc_url( 566 admin_url( 567 'options-general.php?page=noahfirst-ai-assistant-settings' 568 ) 569 ) . 570 '">' . 571 esc_html__('Settings', 'noahfirst-ai-assistant') . 572 '</a>'; 573 array_unshift($links, $settings_link); 574 return $links; 575 } 576 ); 577 578 function noahfirst_ai_assistant_base64url_encode($value) { 579 return rtrim(strtr(base64_encode($value), '+/', '-_'), '='); 580 } 581 582 function noahfirst_ai_assistant_base64url_decode($value) { 583 $padding = strlen($value) % 4; 584 if ($padding) { 585 $value .= str_repeat('=', 4 - $padding); 586 } 587 588 return base64_decode(strtr($value, '-_', '+/'), true); 589 } 590 591 function noahfirst_ai_assistant_encrypt_secret($value) { 592 $value = (string) $value; 593 594 if ($value === '' || strpos($value, 'enc:v1:') === 0) { 595 return $value; 596 } 597 598 if (!function_exists('openssl_encrypt')) { 599 return ''; 600 } 601 602 try { 603 $key = hash('sha256', wp_salt('auth'), true); 604 $iv = random_bytes(12); 605 $tag = ''; 606 $ciphertext = openssl_encrypt( 607 $value, 608 'aes-256-gcm', 609 $key, 610 OPENSSL_RAW_DATA, 611 $iv, 612 $tag 613 ); 614 615 if ($ciphertext === false || $tag === '') { 616 return ''; 617 } 618 619 return implode( 620 ':', 621 array( 622 'enc', 623 'v1', 624 noahfirst_ai_assistant_base64url_encode($iv), 625 noahfirst_ai_assistant_base64url_encode($tag), 626 noahfirst_ai_assistant_base64url_encode($ciphertext), 627 ) 628 ); 629 } catch (Exception $e) { 630 return ''; 631 } 632 } 633 634 function noahfirst_ai_assistant_decrypt_secret($value) { 635 $value = (string) $value; 636 637 if ($value === '' || strpos($value, 'enc:v1:') !== 0) { 638 return $value; 639 } 640 641 if (!function_exists('openssl_decrypt')) { 642 return ''; 643 } 644 645 $parts = explode(':', $value); 646 if (count($parts) !== 5 || $parts[0] !== 'enc' || $parts[1] !== 'v1') { 647 return ''; 648 } 649 650 $iv = noahfirst_ai_assistant_base64url_decode($parts[2]); 651 $tag = noahfirst_ai_assistant_base64url_decode($parts[3]); 652 $ciphertext = noahfirst_ai_assistant_base64url_decode($parts[4]); 653 654 if ($iv === false || $tag === false || $ciphertext === false) { 655 return ''; 656 } 657 658 $plaintext = openssl_decrypt( 659 $ciphertext, 660 'aes-256-gcm', 661 hash('sha256', wp_salt('auth'), true), 662 OPENSSL_RAW_DATA, 663 $iv, 664 $tag 665 ); 666 667 return $plaintext === false ? '' : $plaintext; 668 } 669 670 function noahfirst_ai_assistant_get_secret_key() { 671 return noahfirst_ai_assistant_decrypt_secret( 672 get_option('noahfirst_ai_assistant_secret_key', '') 673 ); 674 } 675 676 function noahfirst_ai_assistant_migrate_secret_storage() { 677 $stored = (string) get_option( 678 'noahfirst_ai_assistant_secret_key', 679 '' 680 ); 681 682 if ($stored !== '' && strpos($stored, 'enc:v1:') !== 0) { 683 $encrypted = noahfirst_ai_assistant_encrypt_secret($stored); 684 if ($encrypted !== $stored) { 685 update_option( 686 'noahfirst_ai_assistant_secret_key', 687 $encrypted, 688 false 689 ); 690 } 691 } 692 } 693 add_action('init', 'noahfirst_ai_assistant_migrate_secret_storage'); 694 695 function noahfirst_ai_assistant_sanitize_secret_key($value) { 696 $value = sanitize_text_field($value); 697 698 // Leaving the password field blank keeps the existing encrypted secret. 699 if ($value === '') { 700 return (string) get_option( 701 'noahfirst_ai_assistant_secret_key', 702 '' 703 ); 704 } 705 706 $encrypted = noahfirst_ai_assistant_encrypt_secret($value); 707 708 if ($encrypted === '') { 709 add_settings_error( 710 'noahfirst_ai_assistant_secret_key', 711 'noahfirst_secret_encryption_failed', 712 __( 713 'Integration secret was not saved because secure encryption is unavailable on this WordPress host.', 714 'noahfirst-ai-assistant' 715 ), 716 'error' 717 ); 718 719 return (string) get_option( 720 'noahfirst_ai_assistant_secret_key', 721 '' 722 ); 723 } 724 725 return $encrypted; 726 } 727 728 function noahfirst_ai_assistant_register_settings() { 729 register_setting( 730 'noahfirst_ai_assistant_settings_group', 731 'noahfirst_ai_assistant_token', 732 array( 733 'type' => 'string', 734 'default' => '', 735 'sanitize_callback' => 'sanitize_text_field', 736 ) 737 ); 738 739 register_setting( 740 'noahfirst_ai_assistant_settings_group', 741 'noahfirst_ai_assistant_secret_key', 742 array( 743 'type' => 'string', 744 'default' => '', 745 'sanitize_callback' => 746 'noahfirst_ai_assistant_sanitize_secret_key', 747 ) 748 ); 749 } 750 add_action('admin_init', 'noahfirst_ai_assistant_register_settings'); 751 752 function noahfirst_ai_assistant_set_admin_notice($status) { 753 if (!in_array($status, array('success', 'error'), true)) { 754 return; 755 } 756 757 set_transient( 758 'noahfirst_ai_assistant_admin_notice', 759 $status, 760 60 761 ); 762 } 763 764 function noahfirst_ai_assistant_get_and_clear_admin_notice() { 765 $status = get_transient('noahfirst_ai_assistant_admin_notice'); 766 767 if ($status !== 'success' && $status !== 'error') { 768 return ''; 769 } 770 771 delete_transient('noahfirst_ai_assistant_admin_notice'); 772 773 return $status; 774 } 775 776 add_action( 777 'admin_post_noahfirst_ai_assistant_resync_products', 778 function () { 779 if (!current_user_can('manage_options')) { 780 wp_die( 781 esc_html__( 782 'You do not have permission to do that.', 783 'noahfirst-ai-assistant' 784 ) 785 ); 786 } 787 788 check_admin_referer( 789 'noahfirst_ai_assistant_resync_products' 790 ); 791 792 $result = noahfirst_ai_assistant_sync_store_to_api(); 793 noahfirst_ai_assistant_set_admin_notice( 794 $result ? 'success' : 'error' 795 ); 796 797 $redirect = add_query_arg( 798 array( 799 'page' => 'noahfirst-ai-assistant-settings', 800 ), 801 admin_url('options-general.php') 802 ); 803 804 wp_safe_redirect($redirect); 805 exit; 806 } 807 ); 808 809 add_action('admin_notices', function () { 810 $screen = function_exists('get_current_screen') 811 ? get_current_screen() 812 : null; 813 814 if (!$screen || $screen->id !== 'settings_page_noahfirst-ai-assistant-settings') { 815 return; 816 } 817 818 $status = noahfirst_ai_assistant_get_and_clear_admin_notice(); 819 820 if ($status === 'success') { 821 echo '<div class="notice notice-success is-dismissible"><p>' . 822 esc_html__( 823 'Store knowledge synced successfully.', 824 'noahfirst-ai-assistant' 825 ) . 826 '</p></div>'; 827 return; 828 } 829 830 if ($status === 'error') { 831 echo '<div class="notice notice-error is-dismissible"><p>' . 832 esc_html__( 833 'Store sync failed. The last known good catalog remains active in NoahFirst.', 834 'noahfirst-ai-assistant' 835 ) . 836 '</p></div>'; 837 } 43 838 }); 44 839 45 46 // Register the setting fields during admin_init 47 function noahfirst_ai_assistant_register_settings() 48 { 49 register_setting('noahfirst_ai_assistant_settings_group', 'noahfirst_ai_assistant_token', array( 50 'type' => 'string', 51 'default' => '', 52 'sanitize_callback' => 'sanitize_text_field' 53 )); 54 } 55 add_action('admin_init', 'noahfirst_ai_assistant_register_settings'); 56 57 58 // Register settings page 59 function noahfirst_ai_assistant_register_settings_page() 60 { 840 function noahfirst_ai_assistant_register_settings_page() { 61 841 add_options_page( 62 842 'NoahFirst AI Assistant', … … 67 847 ); 68 848 } 69 add_action('admin_menu', 'noahfirst_ai_assistant_register_settings_page'); 70 71 72 // Render settings page 73 function noahfirst_ai_assistant_render_settings_page() 74 { 849 add_action( 850 'admin_menu', 851 'noahfirst_ai_assistant_register_settings_page' 852 ); 853 854 function noahfirst_ai_assistant_render_settings_page() { 855 if (!current_user_can('manage_options')) { 856 return; 857 } 858 859 $token = get_option('noahfirst_ai_assistant_token', ''); 860 $has_secret = 861 get_option('noahfirst_ai_assistant_secret_key', '') !== ''; 862 $last_sync = get_option( 863 'noahfirst_ai_assistant_last_sync_at', 864 '' 865 ); 866 $last_status = get_option( 867 'noahfirst_ai_assistant_last_sync_status', 868 '' 869 ); 75 870 ?> 76 <div style="padding:32px ">77 <img width="256 px" src="<?php echo esc_url(plugin_dir_url(__FILE__)) . 'noahfirst-logo.png' ?>" />78 79 < !-- Mandatory Settings -->80 < h2 style="margin-top:32px"><?php esc_html_e('Welcome! Let\'s connect your account', 'noahfirst-ai-assistant'); ?></h2>81 <div>82 <?php esc_html_e('Obtain the token from your account on noahfirst.com. Go to ', 'noahfirst-ai-assistant'); ?><a href="https://noahfirst.com/panel"><?php esc_html_e('your panel', 'noahfirst-ai-assistant'); ?></a><?php esc_html_e(" > NoahFirst AI Assistant > General Settings. If you don't have an account,", 'noahfirst-ai-assistant'); ?> <a href="https://noahfirst.com/sign-up"><?php esc_html_e("register here", 'noahfirst-ai-assistant'); ?></a>.83 </div> 871 <div style="padding:32px;max-width:960px"> 872 <img width="256" src="<?php echo esc_url(plugin_dir_url(__FILE__) . 'noahfirst-logo.png'); ?>" alt="NoahFirst" /> 873 874 <h2 style="margin-top:32px"><?php esc_html_e('Connect this store to NoahFirst', 'noahfirst-ai-assistant'); ?></h2> 875 <p> 876 <?php esc_html_e('Copy the Store Token and Integration Secret from NoahFirst → Store, paste them here, then save. Products and WordPress posts will sync automatically.', 'noahfirst-ai-assistant'); ?> 877 </p> 878 84 879 <form method="post" action="options.php"> 85 880 <?php 86 if (!current_user_can('manage_options')) {87 return;88 }89 90 881 settings_fields('noahfirst_ai_assistant_settings_group'); 91 882 do_settings_sections('noahfirst_ai_assistant_settings_group'); 92 $token = get_option('noahfirst_ai_assistant_token', '');93 883 ?> 94 884 <table class="form-table" role="presentation"> 95 885 <tr> 96 886 <th scope="row"> 97 <label for="noahfirst_ai_assistant_token"><?php esc_html_e(' Token', 'noahfirst-ai-assistant'); ?></label>887 <label for="noahfirst_ai_assistant_token"><?php esc_html_e('Store token', 'noahfirst-ai-assistant'); ?></label> 98 888 </th> 99 889 <td> 100 <input name="noahfirst_ai_assistant_token" type="text" id="noahfirst_ai_assistant_token" value="<?php echo esc_attr($token); ?>" /> 101 <p class="description"><?php esc_html_e('The token from your NoahFirst account.', 'noahfirst-ai-assistant'); ?></p> 890 <input name="noahfirst_ai_assistant_token" type="text" id="noahfirst_ai_assistant_token" value="<?php echo esc_attr($token); ?>" class="regular-text code" autocomplete="off" /> 891 <p class="description"><?php esc_html_e('Identifies this NoahFirst store. It is not the integration secret.', 'noahfirst-ai-assistant'); ?></p> 892 </td> 893 </tr> 894 <tr> 895 <th scope="row"> 896 <label for="noahfirst_ai_assistant_secret_key"><?php esc_html_e('Integration secret', 'noahfirst-ai-assistant'); ?></label> 897 </th> 898 <td> 899 <input name="noahfirst_ai_assistant_secret_key" type="password" id="noahfirst_ai_assistant_secret_key" value="" class="regular-text code" autocomplete="new-password" placeholder="<?php echo esc_attr($has_secret ? __('Configured — leave blank to keep', 'noahfirst-ai-assistant') : __('Paste secret from NoahFirst', 'noahfirst-ai-assistant')); ?>" /> 900 <p class="description"><?php esc_html_e('Stored only in WordPress and validated against a one-way hash in NoahFirst.', 'noahfirst-ai-assistant'); ?></p> 102 901 </td> 103 902 </tr> 104 903 </table> 105 <?php submit_button( ); ?>904 <?php submit_button(__('Save & connect', 'noahfirst-ai-assistant')); ?> 106 905 </form> 107 <!-- End of Mandatory Settings --> 906 907 <hr style="margin:32px 0" /> 908 909 <h2><?php esc_html_e('Store sync', 'noahfirst-ai-assistant'); ?></h2> 910 <p> 911 <?php 912 if ($last_sync) { 913 echo esc_html( 914 sprintf( 915 /* translators: last attempt of products sync */ 916 __('Last attempt: %1$s · %2$s', 'noahfirst-ai-assistant'), 917 $last_sync, 918 $last_status ?: 'unknown' 919 ) 920 ); 921 } else { 922 esc_html_e('No sync has run yet.', 'noahfirst-ai-assistant'); 923 } 924 ?> 925 </p> 926 927 <form method="post" action="<?php echo esc_url(admin_url('admin-post.php')); ?>"> 928 <?php wp_nonce_field('noahfirst_ai_assistant_resync_products'); ?> 929 <input type="hidden" name="action" value="noahfirst_ai_assistant_resync_products" /> 930 <button type="submit" class="button button-secondary"> 931 <?php esc_html_e('Sync now', 'noahfirst-ai-assistant'); ?> 932 </button> 933 </form> 108 934 </div> 109 935 <?php -
noahfirst-ai-assistant/trunk/readme.txt
r3536497 r3737499 1 === NoahFirst AI Assistant===1 === NoahFirst AI Sales Associate === 2 2 Contributors: noahfirstai, noahfirst 3 Tags: Chatbot, AI Agent, live chat, woocommerce, ecommerce3 Tags: woocommerce, ecommerce, ai sales associate, customer support, live chat 4 4 Requires at least: 5.0 5 5 Requires PHP: 7.1 6 Tested up to: 6.97 Stable tag: 1. 0.06 Tested up to: 7.1 7 Stable tag: 1.1.0 8 8 License: GPLv2 or later 9 9 License URI: https://www.gnu.org/licenses/gpl-2.0.html 10 Boost your ecommerce sales with an AI chatbot that understands your store data and turns conversations into customers.10 AI sales associate for online stores that answers buyer questions using your own store data. 11 11 12 12 == Description == 13 13 14 Try NoahFirst free for 30 days and give your store a 24/7 AI assistant that supports 15 customers instantly and helps turn visitors into buyers. 14 NoahFirst is an AI sales associate for independent online stores. It uses your own 15 store knowledge—products, inventory, product details, and published WordPress 16 content—to answer buyer questions while they decide. 16 17 17 NoahFirst is an AI assistant designed for e-commerce stores running on WordPress. 18 Unlike generic bots that rely on external data or guesswork, NoahFirst uses your store’s 19 own data—products, inventory, and policies—to deliver accurate, on-brand 20 answers. It’s like having a digital version of your best store employee—one who 21 knows your catalog inside and out, know your customer the best 18 Unlike a generic chatbot, NoahFirst is designed around ecommerce buying questions. 19 The official WordPress plugin connects the store to NoahFirst, loads the storefront 20 assistant, and keeps structured WooCommerce product knowledge synchronized. 22 21 23 22 == Why NoahFirst? == 24 23 25 NoahFirst works differently from traditional chat tools. Instead of relying on 26 scripts or external data, it uses your store’s own products, inventory, and 27 policies as its source of truth—so every answer stays accurate, consistent, 28 and aligned with your brand. 24 NoahFirst works from your store data rather than guessing from unrelated sources. 25 Structured WooCommerce product facts such as price, stock status, SKU, variants, 26 attributes, and canonical product URLs remain authoritative for shopper answers. 29 27 30 Because NoahFirst operates directly on your store data, it can respond instantly 31 without waiting for human input, giving customers the clarity they need to 32 make decisions without delay. 28 Customers ask. NoahFirst answers. Sales happen. 33 29 34 30 == Key Features == 35 31 36 * Content-Bound Intelligence: Uses your WooCommerce store data (products, inventory, policies) as the primary knowledge source 37 * Real-Time Data Sync: Automatically reflects updates in stock, pricing, and product details without manual retraining 38 * Context-Aware Response Engine: Understands customer intent and generates relevant answers based on your store data 39 * Multi-Language Support: Responds to customers in multiple languages based on their input 40 * Ecommerce-Native Integration: Connects directly with your store structure for accurate and consistent responses 41 * Lightweight Script Loading: Runs asynchronously without blocking your site performance 42 * No-Code Setup: Install and connect your store without manual training or configuration 32 * Store-grounded answers: Uses WooCommerce products and published WordPress content as store knowledge 33 * Automatic store sync: Product, inventory, variation, pricing, and post changes schedule a NoahFirst sync 34 * Scheduled safety sync: A twice-daily heartbeat refreshes connected store knowledge 35 * Manual resync: Store administrators can trigger a sync and see the latest sync status 36 * Structured commerce ingestion: Sends product facts directly to NoahFirst without WooCommerce REST consumer credentials 37 * Last-known-good protection: NoahFirst keeps the prior successful catalog if an ingestion attempt fails 38 * Secure connection settings: The Integration Secret is encrypted at rest inside WordPress 39 * Multi-language shopper responses: NoahFirst can respond based on the shopper's language 40 * Lightweight storefront loader: The assistant script loads on connected storefronts 41 * No-code setup: Install the plugin, connect the store, and let NoahFirst handle ongoing sync 43 42 44 43 == How It Works == 45 44 46 45 1. Install and activate the NoahFirst plugin 47 2. Connect your NoahFirst account 48 3. NoahFirst syncs with your WooCommerce store data 49 4. Your AI assistant is ready to answer customers instantly 46 2. Copy the Store Token and Integration Secret from NoahFirst → Store 47 3. Save the connection in WordPress 48 4. NoahFirst performs the initial store sync 49 5. Product, stock, variation, and post changes schedule future syncs automatically 50 6. The storefront assistant can answer shoppers using the connected store knowledge 50 51 51 52 == Use Cases == 52 53 53 * When customers ask about product details, NoahFirst answers instantly using your store data54 * When shoppers check sizing, availability, or delivery, NoahFirst provides accurate responses in real time55 * When your team is unavailable, NoahFirst continues supporting customers 24/756 * When visitors hesitate before buying, NoahFirst answers their questions and helps them move forward57 * During high traffic or campaigns, NoahFirst handles multiple conversations at once without delay54 * Product-detail and product-selection questions 55 * Size, compatibility, availability, and usage questions 56 * Product comparison and catalog guidance 57 * Store-policy questions supported by published store knowledge 58 * Shopper questions when the merchant team is unavailable 58 59 59 60 == Frequently Asked Questions == 60 61 61 62 = Does NoahFirst require WooCommerce? = 62 NoahFirst is optimized for WooCommerce stores but can also work on general WordPress websites. 63 NoahFirst is optimized for WooCommerce stores. The plugin can also sync published 64 WordPress posts as supporting store knowledge. 63 65 64 66 = Does NoahFirst use my store data? = 65 Yes. NoahFirst uses your store’s own data to provide accurate andrelevant answers.67 Yes. NoahFirst uses connected store knowledge to provide relevant answers. 66 68 67 = Does NoahFirst slow down my website? = 68 No. NoahFirst is lightweight and designed to run without affecting your site performance. 69 = How does automatic sync work? = 70 Connected stores schedule a sync when WooCommerce products, variations, stock, or 71 published WordPress posts change. A scheduled heartbeat also refreshes the store 72 periodically. Administrators can trigger a manual sync from the plugin settings page. 73 74 = Does NoahFirst use WooCommerce REST API consumer keys? = 75 No. The official plugin pushes structured store data to NoahFirst using the Store 76 Token and Integration Secret configured by the administrator. 69 77 70 78 = Is technical setup required? = 71 No. NoahFirst is designed to work with minimal setup and no coding required. 72 73 = Will the AI sound like a robot? = 74 No. NoahFirst adapts to your brand voice. Whether your store is professional, playful, or premium, it responds in a way that feels natural and aligned with your brand. 75 76 = Does the trial require a credit card? = 77 No. You can start your 30-day free trial with no credit card required. 79 No coding is required. A store administrator installs the plugin and connects the 80 credentials supplied by NoahFirst. 78 81 79 82 == External Services == 80 83 81 This plugin connects to the NoahFirst API (https://api.noahfirst.com) to provide AI-powered responses within your store. 84 This plugin connects to the NoahFirst API (https://api.noahfirst.com) and loads the 85 NoahFirst storefront assistant script from NoahFirst infrastructure. 82 86 83 * Service: NoahFirst API 84 * Purpose: Processes customer queries and store data to generate real-time responses. 85 * Data Sent: Customer messages entered in the chat widget and relevant store data (such as product details, inventory, and store policies). 86 * When Data is Sent: Data is transmitted only when a user submits a query through the chat interface or when the assistant is triggered to generate a response. 87 * Subprocessors: This service relies on OpenAI (AI processing provider) and IDCloudHost (infrastructure provider). Data is processed solely to generate real-time responses and is not used to train shared or public AI models. All subprocessors are bound by data processing agreements requiring confidentiality and secure handling of data. 88 * Security: All data is transmitted securely using HTTPS (TLS encryption). Data at rest is protected using infrastructure-level security controls. 87 Service: NoahFirst API 88 Purpose: Authenticates the connected store, receives structured store knowledge, 89 and powers shopper-facing NoahFirst responses. 90 Data Sent During Store Sync: Store Token, Integration Secret for request 91 authentication, site URL, WooCommerce product and variation data (including names, 92 descriptions, prices, stock, SKUs, attributes, categories, and product URLs), and 93 published WordPress post content selected by the plugin. 94 When Store Data Is Sent: After connection settings are saved, when relevant 95 WooCommerce product/stock/variation or WordPress post changes schedule a sync, 96 during the scheduled store-sync heartbeat, or when an administrator chooses 97 "Sync now". 98 Shopper Data: When a shopper uses the NoahFirst storefront assistant, the widget 99 communicates with the NoahFirst service to process that shopper conversation. 100 Subprocessors: NoahFirst uses OpenAI for AI processing and IDCloudHost for 101 infrastructure hosting. 102 Security: Data is transmitted over HTTPS. The WordPress Integration Secret is 103 stored encrypted by the plugin using the WordPress installation's authentication salt. 89 104 90 105 Policies: 91 *Privacy Policy: https://noahfirst.com/privacy92 *Terms of Service: https://noahfirst.com/terms93 *Security: https://noahfirst.com/security106 Privacy Policy: https://noahfirst.com/privacy 107 Terms of Service: https://noahfirst.com/terms 108 Security: https://noahfirst.com/security 94 109 95 110 == Screenshots == 96 111 97 1. NoahFirst can answer a product based on its information 98 2. NoahFirst can answer about delivery information 99 3. NoahFirst can recommend a product based on customer's needs 100 4. NoahFirst can answer payment information 101 5. NoahFirst can recommend a product based on customer's needs 102 6. NoahFirst can recommend a product based on customer's needs 103 7. NoahFirst understands your product 104 8. NoahFirst can recommend a product based on customer's needs 112 1. NoahFirst answers product questions using connected store information 113 2. NoahFirst supports delivery and policy questions when the store provides that knowledge 114 3. NoahFirst helps shoppers compare and select products 115 4. NoahFirst can answer availability and product-detail questions 105 116 106 117 == Changelog == 118 119 = 1.1.0 = 120 121 * Added structured WooCommerce product and variation ingestion 122 * Added automatic sync hooks for product, stock, variation, and WordPress post changes 123 * Added twice-daily sync heartbeat and manual "Sync now" control 124 * Added sync status visibility for store administrators 125 * Added encrypted storage for the Integration Secret 126 * Added last-known-good catalog protection through the NoahFirst ingestion contract 127 * Added real WordPress and WooCommerce runtime verification in CI 107 128 108 129 = 1.0.0 = … … 112 133 == Upgrade Notice == 113 134 114 = 1.0.0 = 115 Initial release 135 = 1.1.0 = 136 Adds automatic structured store sync and secure connection storage for the official 137 NoahFirst WordPress integration.
Note:
See TracChangeset
for help on using the changeset viewer.
![(please configure the [header_logo] section in trac.ini)](/chrome/site/your_project_logo.png)