Plugin Directory

Changeset 3737499


Ignore:
Timestamp:
10/10/2026 01:28:36 AM (2 weeks ago)
Author:
noahfirstai
Message:

Release version 1.1.0

Location:
noahfirst-ai-assistant
Files:
4 edited
1 copied

Legend:

Unmodified
Added
Removed
  • noahfirst-ai-assistant/tags/1.1.0/noahfirst-ai-assistant.php

    r3534632 r3737499  
    22
    33/**
    4  * Plugin Name: NoahFirst AI Assistant
    5  * Description: Boost your ecommerce sales with an AI chatbot that understands your store data and turns conversations into customers.
    6  * Version: 1.0.0
     4 * 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
    77 * License: GPLv2 or later
    88 * Author: NoahFirstAI
    … …  
    1212defined('ABSPATH') || exit;
    1313
    14 // Add NoahFirst Widget script
     14const NOAHFIRST_AI_ASSISTANT_SYNC_HOOK = 'noahfirst_ai_assistant_run_store_sync';
     15
     16function 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
     23function 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
     30function 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
     72function 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
     112function 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
     126function 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
     168function 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
     231function 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
     266function noahfirst_ai_assistant_push_catalog($token, $secret_key, $products) {
     267  $query = <<<'GRAPHQL'
     268mutation 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}
     278GRAPHQL;
     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
     303function noahfirst_ai_assistant_push_posts($token, $secret_key, $posts) {
     304  $query = <<<'GRAPHQL'
     305mutation IngestPlatformContent($input: PlatformContentIngestionInput!) {
     306  ingestPlatformContentByToken(input: $input) {
     307    accepted
     308    content_type
     309    item_count
     310  }
     311}
     312GRAPHQL;
     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
     337function 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
     379function 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
     395add_action(
     396  NOAHFIRST_AI_ASSISTANT_SYNC_HOOK,
     397  'noahfirst_ai_assistant_sync_store_to_api'
     398);
     399
     400const NOAHFIRST_AI_ASSISTANT_HEARTBEAT_HOOK = 'noahfirst_ai_assistant_store_sync_heartbeat';
     401
     402function 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
     412function 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
     424register_activation_hook(
     425  __FILE__,
     426  'noahfirst_ai_assistant_register_heartbeat'
     427);
     428register_deactivation_hook(
     429  __FILE__,
     430  'noahfirst_ai_assistant_clear_heartbeat'
     431);
     432
     433add_action(
     434  'init',
     435  'noahfirst_ai_assistant_register_heartbeat'
     436);
     437
     438add_action(
     439  NOAHFIRST_AI_ASSISTANT_HEARTBEAT_HOOK,
     440  'noahfirst_ai_assistant_sync_store_to_api'
     441);
     442
     443function noahfirst_ai_assistant_schedule_woocommerce_sync() {
     444  noahfirst_ai_assistant_schedule_sync(5);
     445}
     446
     447add_action(
     448  'woocommerce_update_product',
     449  'noahfirst_ai_assistant_schedule_woocommerce_sync',
     450  20,
     451  1
     452);
     453add_action(
     454  'woocommerce_new_product',
     455  'noahfirst_ai_assistant_schedule_woocommerce_sync',
     456  20,
     457  1
     458);
     459add_action(
     460  'woocommerce_product_set_stock',
     461  'noahfirst_ai_assistant_schedule_woocommerce_sync',
     462  20,
     463  1
     464);
     465add_action(
     466  'woocommerce_variation_set_stock',
     467  'noahfirst_ai_assistant_schedule_woocommerce_sync',
     468  20,
     469  1
     470);
     471add_action(
     472  'woocommerce_product_set_stock_status',
     473  'noahfirst_ai_assistant_schedule_woocommerce_sync',
     474  20,
     475  1
     476);
     477add_action(
     478  'woocommerce_variation_set_stock_status',
     479  'noahfirst_ai_assistant_schedule_woocommerce_sync',
     480  20,
     481  1
     482);
     483
     484function 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
     502add_action(
     503  'save_post',
     504  'noahfirst_ai_assistant_maybe_schedule_sync',
     505  30,
     506  1
     507);
     508
     509add_action('trashed_post', function ($post_id) {
     510  noahfirst_ai_assistant_schedule_sync();
     511}, 30, 1);
     512
     513add_action('untrashed_post', function ($post_id) {
     514  noahfirst_ai_assistant_schedule_sync();
     515}, 30, 1);
     516
     517add_action('before_delete_post', function ($post_id) {
     518  noahfirst_ai_assistant_schedule_sync();
     519}, 30, 1);
     520
     521add_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.
    15538add_action('wp_footer', function () {
    16539  $token = get_option('noahfirst_ai_assistant_token', '');
    … …  
    23546    'token',
    24547    rawurlencode($token),
    25     'https://api.noahfirst.com/assets/scripts/noah/prod.tanyanoah.v0.0.2.js'
     548    noahfirst_ai_assistant_get_widget_script_url()
    26549  );
    27550
    28551  wp_enqueue_script(
    29     'noahfirst-ai-assistant-script', // handle
    30     esc_url($script_url), // path
    31     [], // dependencies
     552    'noahfirst-ai-assistant-script',
     553    esc_url($script_url),
     554    array(),
    32555    '0.0.2',
    33     true // load in footer
     556    true
    34557  );
    35558});
    36559
    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;
     560add_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
     578function noahfirst_ai_assistant_base64url_encode($value) {
     579  return rtrim(strtr(base64_encode($value), '+/', '-_'), '=');
     580}
     581
     582function 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
     591function 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
     634function 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
     670function noahfirst_ai_assistant_get_secret_key() {
     671  return noahfirst_ai_assistant_decrypt_secret(
     672    get_option('noahfirst_ai_assistant_secret_key', '')
     673  );
     674}
     675
     676function 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}
     693add_action('init', 'noahfirst_ai_assistant_migrate_secret_storage');
     694
     695function 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
     728function 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}
     750add_action('admin_init', 'noahfirst_ai_assistant_register_settings');
     751
     752function 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
     764function 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
     776add_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
     809add_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  }
    43838});
    44839
    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 {
     840function noahfirst_ai_assistant_register_settings_page() {
    61841  add_options_page(
    62842    'NoahFirst AI Assistant',
    … …  
    67847  );
    68848}
    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 {
     849add_action(
     850  'admin_menu',
     851  'noahfirst_ai_assistant_register_settings_page'
     852);
     853
     854function 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  );
    75870?>
    76   <div style="padding:32px">
    77     <img width="256px" 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
    84879    <form method="post" action="options.php">
    85880      <?php
    86       if (!current_user_can('manage_options')) {
    87         return;
    88       }
    89 
    90881      settings_fields('noahfirst_ai_assistant_settings_group');
    91882      do_settings_sections('noahfirst_ai_assistant_settings_group');
    92       $token = get_option('noahfirst_ai_assistant_token', '');
    93883      ?>
    94884      <table class="form-table" role="presentation">
    95885        <tr>
    96886          <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>
    98888          </th>
    99889          <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>
    102901          </td>
    103902        </tr>
    104903      </table>
    105       <?php submit_button(); ?>
     904      <?php submit_button(__('Save & connect', 'noahfirst-ai-assistant')); ?>
    106905    </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>
    108934  </div>
    109935<?php
  • noahfirst-ai-assistant/tags/1.1.0/readme.txt

    r3536497 r3737499  
    1 ===  NoahFirst AI Assistant ===
     1=== NoahFirst AI Sales Associate ===
    22Contributors: noahfirstai, noahfirst
    3 Tags: Chatbot, AI Agent, live chat, woocommerce, ecommerce
     3Tags: woocommerce, ecommerce, ai sales associate, customer support, live chat
    44Requires at least: 5.0
    55Requires PHP: 7.1
    6 Tested up to: 6.9
    7 Stable tag: 1.0.0
     6Tested up to: 7.1
     7Stable tag: 1.1.0
    88License: GPLv2 or later
    99License 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.
     10AI sales associate for online stores that answers buyer questions using your own store data.
    1111
    1212== Description ==
    1313
    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.
     14NoahFirst is an AI sales associate for independent online stores. It uses your own
     15store knowledge—products, inventory, product details, and published WordPress
     16content—to answer buyer questions while they decide.
    1617
    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
     18Unlike a generic chatbot, NoahFirst is designed around ecommerce buying questions.
     19The official WordPress plugin connects the store to NoahFirst, loads the storefront
     20assistant, and keeps structured WooCommerce product knowledge synchronized.
    2221
    2322== Why NoahFirst? ==
    2423
    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.
     24NoahFirst works from your store data rather than guessing from unrelated sources.
     25Structured WooCommerce product facts such as price, stock status, SKU, variants,
     26attributes, and canonical product URLs remain authoritative for shopper answers.
    2927
    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.
     28Customers ask. NoahFirst answers. Sales happen.
    3329
    3430== Key Features ==
    3531
    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
    4342
    4443== How It Works ==
    4544
    46451. 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
     462. Copy the Store Token and Integration Secret from NoahFirst → Store
     473. Save the connection in WordPress
     484. NoahFirst performs the initial store sync
     495. Product, stock, variation, and post changes schedule future syncs automatically
     506. The storefront assistant can answer shoppers using the connected store knowledge
    5051
    5152== Use Cases ==
    5253
    53 * When customers ask about product details, NoahFirst answers instantly using your store data
    54 * When shoppers check sizing, availability, or delivery, NoahFirst provides accurate responses in real time
    55 * When your team is unavailable, NoahFirst continues supporting customers 24/7
    56 * When visitors hesitate before buying, NoahFirst answers their questions and helps them move forward
    57 * During high traffic or campaigns, NoahFirst handles multiple conversations at once without delay
     54* 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
    5859
    5960== Frequently Asked Questions ==
    6061
    6162= Does NoahFirst require WooCommerce? =
    62 NoahFirst is optimized for WooCommerce stores but can also work on general WordPress websites.
     63NoahFirst is optimized for WooCommerce stores. The plugin can also sync published
     64WordPress posts as supporting store knowledge.
    6365
    6466= Does NoahFirst use my store data? =
    65 Yes. NoahFirst uses your store’s own data to provide accurate and relevant answers.
     67Yes. NoahFirst uses connected store knowledge to provide relevant answers.
    6668
    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? =
     70Connected stores schedule a sync when WooCommerce products, variations, stock, or
     71published WordPress posts change. A scheduled heartbeat also refreshes the store
     72periodically. Administrators can trigger a manual sync from the plugin settings page.
     73
     74= Does NoahFirst use WooCommerce REST API consumer keys? =
     75No. The official plugin pushes structured store data to NoahFirst using the Store
     76Token and Integration Secret configured by the administrator.
    6977
    7078= 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.
     79No coding is required. A store administrator installs the plugin and connects the
     80credentials supplied by NoahFirst.
    7881
    7982== External Services ==
    8083
    81 This plugin connects to the NoahFirst API (https://api.noahfirst.com) to provide AI-powered responses within your store. 
     84This plugin connects to the NoahFirst API (https://api.noahfirst.com) and loads the
     85NoahFirst storefront assistant script from NoahFirst infrastructure.
    8286
    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.
     87Service: NoahFirst API
     88Purpose: Authenticates the connected store, receives structured store knowledge,
     89and powers shopper-facing NoahFirst responses.
     90Data Sent During Store Sync: Store Token, Integration Secret for request
     91authentication, site URL, WooCommerce product and variation data (including names,
     92descriptions, prices, stock, SKUs, attributes, categories, and product URLs), and
     93published WordPress post content selected by the plugin.
     94When Store Data Is Sent: After connection settings are saved, when relevant
     95WooCommerce product/stock/variation or WordPress post changes schedule a sync,
     96during the scheduled store-sync heartbeat, or when an administrator chooses
     97"Sync now".
     98Shopper Data: When a shopper uses the NoahFirst storefront assistant, the widget
     99communicates with the NoahFirst service to process that shopper conversation.
     100Subprocessors: NoahFirst uses OpenAI for AI processing and IDCloudHost for
     101infrastructure hosting.
     102Security: Data is transmitted over HTTPS. The WordPress Integration Secret is
     103stored encrypted by the plugin using the WordPress installation's authentication salt.
    89104
    90105Policies:
    91 * Privacy Policy: https://noahfirst.com/privacy
    92 * Terms of Service: https://noahfirst.com/terms
    93 * Security: https://noahfirst.com/security
     106Privacy Policy: https://noahfirst.com/privacy
     107Terms of Service: https://noahfirst.com/terms
     108Security: https://noahfirst.com/security
    94109
    95110== Screenshots ==
    96111
    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
     1121. NoahFirst answers product questions using connected store information
     1132. NoahFirst supports delivery and policy questions when the store provides that knowledge
     1143. NoahFirst helps shoppers compare and select products
     1154. NoahFirst can answer availability and product-detail questions
    105116
    106117== 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
    107128
    108129= 1.0.0 =
    … …  
    112133== Upgrade Notice ==
    113134
    114 = 1.0.0 =
    115 Initial release
     135= 1.1.0 =
     136Adds automatic structured store sync and secure connection storage for the official
     137NoahFirst WordPress integration.
  • noahfirst-ai-assistant/trunk/noahfirst-ai-assistant.php

    r3534632 r3737499  
    22
    33/**
    4  * Plugin Name: NoahFirst AI Assistant
    5  * Description: Boost your ecommerce sales with an AI chatbot that understands your store data and turns conversations into customers.
    6  * Version: 1.0.0
     4 * 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
    77 * License: GPLv2 or later
    88 * Author: NoahFirstAI
    … …  
    1212defined('ABSPATH') || exit;
    1313
    14 // Add NoahFirst Widget script
     14const NOAHFIRST_AI_ASSISTANT_SYNC_HOOK = 'noahfirst_ai_assistant_run_store_sync';
     15
     16function 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
     23function 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
     30function 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
     72function 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
     112function 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
     126function 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
     168function 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
     231function 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
     266function noahfirst_ai_assistant_push_catalog($token, $secret_key, $products) {
     267  $query = <<<'GRAPHQL'
     268mutation 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}
     278GRAPHQL;
     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
     303function noahfirst_ai_assistant_push_posts($token, $secret_key, $posts) {
     304  $query = <<<'GRAPHQL'
     305mutation IngestPlatformContent($input: PlatformContentIngestionInput!) {
     306  ingestPlatformContentByToken(input: $input) {
     307    accepted
     308    content_type
     309    item_count
     310  }
     311}
     312GRAPHQL;
     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
     337function 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
     379function 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
     395add_action(
     396  NOAHFIRST_AI_ASSISTANT_SYNC_HOOK,
     397  'noahfirst_ai_assistant_sync_store_to_api'
     398);
     399
     400const NOAHFIRST_AI_ASSISTANT_HEARTBEAT_HOOK = 'noahfirst_ai_assistant_store_sync_heartbeat';
     401
     402function 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
     412function 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
     424register_activation_hook(
     425  __FILE__,
     426  'noahfirst_ai_assistant_register_heartbeat'
     427);
     428register_deactivation_hook(
     429  __FILE__,
     430  'noahfirst_ai_assistant_clear_heartbeat'
     431);
     432
     433add_action(
     434  'init',
     435  'noahfirst_ai_assistant_register_heartbeat'
     436);
     437
     438add_action(
     439  NOAHFIRST_AI_ASSISTANT_HEARTBEAT_HOOK,
     440  'noahfirst_ai_assistant_sync_store_to_api'
     441);
     442
     443function noahfirst_ai_assistant_schedule_woocommerce_sync() {
     444  noahfirst_ai_assistant_schedule_sync(5);
     445}
     446
     447add_action(
     448  'woocommerce_update_product',
     449  'noahfirst_ai_assistant_schedule_woocommerce_sync',
     450  20,
     451  1
     452);
     453add_action(
     454  'woocommerce_new_product',
     455  'noahfirst_ai_assistant_schedule_woocommerce_sync',
     456  20,
     457  1
     458);
     459add_action(
     460  'woocommerce_product_set_stock',
     461  'noahfirst_ai_assistant_schedule_woocommerce_sync',
     462  20,
     463  1
     464);
     465add_action(
     466  'woocommerce_variation_set_stock',
     467  'noahfirst_ai_assistant_schedule_woocommerce_sync',
     468  20,
     469  1
     470);
     471add_action(
     472  'woocommerce_product_set_stock_status',
     473  'noahfirst_ai_assistant_schedule_woocommerce_sync',
     474  20,
     475  1
     476);
     477add_action(
     478  'woocommerce_variation_set_stock_status',
     479  'noahfirst_ai_assistant_schedule_woocommerce_sync',
     480  20,
     481  1
     482);
     483
     484function 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
     502add_action(
     503  'save_post',
     504  'noahfirst_ai_assistant_maybe_schedule_sync',
     505  30,
     506  1
     507);
     508
     509add_action('trashed_post', function ($post_id) {
     510  noahfirst_ai_assistant_schedule_sync();
     511}, 30, 1);
     512
     513add_action('untrashed_post', function ($post_id) {
     514  noahfirst_ai_assistant_schedule_sync();
     515}, 30, 1);
     516
     517add_action('before_delete_post', function ($post_id) {
     518  noahfirst_ai_assistant_schedule_sync();
     519}, 30, 1);
     520
     521add_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.
    15538add_action('wp_footer', function () {
    16539  $token = get_option('noahfirst_ai_assistant_token', '');
    … …  
    23546    'token',
    24547    rawurlencode($token),
    25     'https://api.noahfirst.com/assets/scripts/noah/prod.tanyanoah.v0.0.2.js'
     548    noahfirst_ai_assistant_get_widget_script_url()
    26549  );
    27550
    28551  wp_enqueue_script(
    29     'noahfirst-ai-assistant-script', // handle
    30     esc_url($script_url), // path
    31     [], // dependencies
     552    'noahfirst-ai-assistant-script',
     553    esc_url($script_url),
     554    array(),
    32555    '0.0.2',
    33     true // load in footer
     556    true
    34557  );
    35558});
    36559
    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;
     560add_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
     578function noahfirst_ai_assistant_base64url_encode($value) {
     579  return rtrim(strtr(base64_encode($value), '+/', '-_'), '=');
     580}
     581
     582function 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
     591function 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
     634function 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
     670function noahfirst_ai_assistant_get_secret_key() {
     671  return noahfirst_ai_assistant_decrypt_secret(
     672    get_option('noahfirst_ai_assistant_secret_key', '')
     673  );
     674}
     675
     676function 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}
     693add_action('init', 'noahfirst_ai_assistant_migrate_secret_storage');
     694
     695function 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
     728function 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}
     750add_action('admin_init', 'noahfirst_ai_assistant_register_settings');
     751
     752function 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
     764function 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
     776add_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
     809add_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  }
    43838});
    44839
    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 {
     840function noahfirst_ai_assistant_register_settings_page() {
    61841  add_options_page(
    62842    'NoahFirst AI Assistant',
    … …  
    67847  );
    68848}
    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 {
     849add_action(
     850  'admin_menu',
     851  'noahfirst_ai_assistant_register_settings_page'
     852);
     853
     854function 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  );
    75870?>
    76   <div style="padding:32px">
    77     <img width="256px" 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
    84879    <form method="post" action="options.php">
    85880      <?php
    86       if (!current_user_can('manage_options')) {
    87         return;
    88       }
    89 
    90881      settings_fields('noahfirst_ai_assistant_settings_group');
    91882      do_settings_sections('noahfirst_ai_assistant_settings_group');
    92       $token = get_option('noahfirst_ai_assistant_token', '');
    93883      ?>
    94884      <table class="form-table" role="presentation">
    95885        <tr>
    96886          <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>
    98888          </th>
    99889          <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>
    102901          </td>
    103902        </tr>
    104903      </table>
    105       <?php submit_button(); ?>
     904      <?php submit_button(__('Save & connect', 'noahfirst-ai-assistant')); ?>
    106905    </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>
    108934  </div>
    109935<?php
  • noahfirst-ai-assistant/trunk/readme.txt

    r3536497 r3737499  
    1 ===  NoahFirst AI Assistant ===
     1=== NoahFirst AI Sales Associate ===
    22Contributors: noahfirstai, noahfirst
    3 Tags: Chatbot, AI Agent, live chat, woocommerce, ecommerce
     3Tags: woocommerce, ecommerce, ai sales associate, customer support, live chat
    44Requires at least: 5.0
    55Requires PHP: 7.1
    6 Tested up to: 6.9
    7 Stable tag: 1.0.0
     6Tested up to: 7.1
     7Stable tag: 1.1.0
    88License: GPLv2 or later
    99License 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.
     10AI sales associate for online stores that answers buyer questions using your own store data.
    1111
    1212== Description ==
    1313
    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.
     14NoahFirst is an AI sales associate for independent online stores. It uses your own
     15store knowledge—products, inventory, product details, and published WordPress
     16content—to answer buyer questions while they decide.
    1617
    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
     18Unlike a generic chatbot, NoahFirst is designed around ecommerce buying questions.
     19The official WordPress plugin connects the store to NoahFirst, loads the storefront
     20assistant, and keeps structured WooCommerce product knowledge synchronized.
    2221
    2322== Why NoahFirst? ==
    2423
    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.
     24NoahFirst works from your store data rather than guessing from unrelated sources.
     25Structured WooCommerce product facts such as price, stock status, SKU, variants,
     26attributes, and canonical product URLs remain authoritative for shopper answers.
    2927
    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.
     28Customers ask. NoahFirst answers. Sales happen.
    3329
    3430== Key Features ==
    3531
    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
    4342
    4443== How It Works ==
    4544
    46451. 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
     462. Copy the Store Token and Integration Secret from NoahFirst → Store
     473. Save the connection in WordPress
     484. NoahFirst performs the initial store sync
     495. Product, stock, variation, and post changes schedule future syncs automatically
     506. The storefront assistant can answer shoppers using the connected store knowledge
    5051
    5152== Use Cases ==
    5253
    53 * When customers ask about product details, NoahFirst answers instantly using your store data
    54 * When shoppers check sizing, availability, or delivery, NoahFirst provides accurate responses in real time
    55 * When your team is unavailable, NoahFirst continues supporting customers 24/7
    56 * When visitors hesitate before buying, NoahFirst answers their questions and helps them move forward
    57 * During high traffic or campaigns, NoahFirst handles multiple conversations at once without delay
     54* 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
    5859
    5960== Frequently Asked Questions ==
    6061
    6162= Does NoahFirst require WooCommerce? =
    62 NoahFirst is optimized for WooCommerce stores but can also work on general WordPress websites.
     63NoahFirst is optimized for WooCommerce stores. The plugin can also sync published
     64WordPress posts as supporting store knowledge.
    6365
    6466= Does NoahFirst use my store data? =
    65 Yes. NoahFirst uses your store’s own data to provide accurate and relevant answers.
     67Yes. NoahFirst uses connected store knowledge to provide relevant answers.
    6668
    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? =
     70Connected stores schedule a sync when WooCommerce products, variations, stock, or
     71published WordPress posts change. A scheduled heartbeat also refreshes the store
     72periodically. Administrators can trigger a manual sync from the plugin settings page.
     73
     74= Does NoahFirst use WooCommerce REST API consumer keys? =
     75No. The official plugin pushes structured store data to NoahFirst using the Store
     76Token and Integration Secret configured by the administrator.
    6977
    7078= 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.
     79No coding is required. A store administrator installs the plugin and connects the
     80credentials supplied by NoahFirst.
    7881
    7982== External Services ==
    8083
    81 This plugin connects to the NoahFirst API (https://api.noahfirst.com) to provide AI-powered responses within your store. 
     84This plugin connects to the NoahFirst API (https://api.noahfirst.com) and loads the
     85NoahFirst storefront assistant script from NoahFirst infrastructure.
    8286
    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.
     87Service: NoahFirst API
     88Purpose: Authenticates the connected store, receives structured store knowledge,
     89and powers shopper-facing NoahFirst responses.
     90Data Sent During Store Sync: Store Token, Integration Secret for request
     91authentication, site URL, WooCommerce product and variation data (including names,
     92descriptions, prices, stock, SKUs, attributes, categories, and product URLs), and
     93published WordPress post content selected by the plugin.
     94When Store Data Is Sent: After connection settings are saved, when relevant
     95WooCommerce product/stock/variation or WordPress post changes schedule a sync,
     96during the scheduled store-sync heartbeat, or when an administrator chooses
     97"Sync now".
     98Shopper Data: When a shopper uses the NoahFirst storefront assistant, the widget
     99communicates with the NoahFirst service to process that shopper conversation.
     100Subprocessors: NoahFirst uses OpenAI for AI processing and IDCloudHost for
     101infrastructure hosting.
     102Security: Data is transmitted over HTTPS. The WordPress Integration Secret is
     103stored encrypted by the plugin using the WordPress installation's authentication salt.
    89104
    90105Policies:
    91 * Privacy Policy: https://noahfirst.com/privacy
    92 * Terms of Service: https://noahfirst.com/terms
    93 * Security: https://noahfirst.com/security
     106Privacy Policy: https://noahfirst.com/privacy
     107Terms of Service: https://noahfirst.com/terms
     108Security: https://noahfirst.com/security
    94109
    95110== Screenshots ==
    96111
    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
     1121. NoahFirst answers product questions using connected store information
     1132. NoahFirst supports delivery and policy questions when the store provides that knowledge
     1143. NoahFirst helps shoppers compare and select products
     1154. NoahFirst can answer availability and product-detail questions
    105116
    106117== 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
    107128
    108129= 1.0.0 =
    … …  
    112133== Upgrade Notice ==
    113134
    114 = 1.0.0 =
    115 Initial release
     135= 1.1.0 =
     136Adds automatic structured store sync and secure connection storage for the official
     137NoahFirst WordPress integration.
Note: See TracChangeset for help on using the changeset viewer.