';
}
/**
* Render a unified settings save footer.
*
* @param string $submit_name Submit button name attribute.
* @param string $submit_label Submit button label.
* @return void
*/
public function render_settings_footer( $submit_name, $submit_label ) {
?>
'click-to-chat-settings',
'ctc-settings-tab' => 'numbers',
);
break;
case 'click-to-chat-templates':
$args = array(
'page' => 'click-to-chat-settings',
'ctc-settings-tab' => 'templates',
);
break;
case 'click-to-chat-shortcodes':
$args = array(
'page' => 'click-to-chat-settings',
'ctc-settings-tab' => 'shortcodes',
);
break;
}
if ( empty( $args ) && 'click-to-chat' === $page && ( isset( $_GET['ctc-tab'] ) || isset( $_GET['tab'] ) ) ) {
$tab = isset( $_GET['ctc-tab'] ) ? sanitize_text_field( wp_unslash( $_GET['ctc-tab'] ) ) : sanitize_text_field( wp_unslash( $_GET['tab'] ) );
$args = array(
'page' => 'click-to-chat-settings',
'ctc-settings-tab' => 'general',
'ctc-tab' => $tab,
);
}
if ( ! empty( $args ) ) {
wp_safe_redirect( add_query_arg( $args, admin_url( 'admin.php' ) ) );
exit;
}
// phpcs:enable WordPress.Security.NonceVerification.Recommended
}
/**
* Render the shared Aicoso product header.
*
* @param array $args Header arguments.
* @return void
*/
public function render_product_header( $args = array() ) {
$args = wp_parse_args(
$args,
array(
'logo_url' => $this->get_logo_url(),
'brand_label' => esc_html__( 'AICOSO Click to Chat', 'aicoso-click-to-chat' ),
'breadcrumb_root' => esc_html__( 'Click to Chat', 'aicoso-click-to-chat' ),
'breadcrumb_parent' => '',
'breadcrumb_parent_url' => '',
'breadcrumb_current' => '',
'actions' => array(),
'has_header_tabs' => false,
)
);
$header_class = $args['has_header_tabs'] ? 'ctc-product-header--with-tabs' : 'ctc-product-header--standalone';
?>
esc_html__( 'Click to Chat navigation', 'aicoso-click-to-chat' ),
'class' => '',
)
);
$classes = trim( 'subsubsub ctc_tablinks ctc-admin-tabs ' . $args['class'] );
?>
'',
'label' => '',
'url' => '',
'icon' => '',
'active' => null,
)
);
$is_active = null === $tab['active'] ? $current === $tab['id'] : (bool) $tab['active'];
$item_class = $is_active ? 'ctc-active' : '';
$aria_current = $is_active ? 'page' : '';
?>
-
>
>
get_settings_tab_slugs();
$tab = 'general';
// phpcs:disable WordPress.Security.NonceVerification.Recommended -- Tab selection is display-only.
if ( isset( $_GET['ctc-tab'] ) ) {
$tab = sanitize_text_field( wp_unslash( $_GET['ctc-tab'] ) );
} elseif ( isset( $_GET['tab'] ) ) {
// Legacy query parameter kept for backward compatibility.
$tab = sanitize_text_field( wp_unslash( $_GET['tab'] ) );
}
// phpcs:enable WordPress.Security.NonceVerification.Recommended
return in_array( $tab, $valid_tabs, true ) ? $tab : 'general';
}
/**
* Render setup warnings when the plugin is not ready.
*
* @return void
*/
private function render_setup_status_notices() {
$plugin_enabled = isset( $this->settings['plugin_enabled'] ) ? $this->settings['plugin_enabled'] : true;
$has_valid_numbers = false;
if ( isset( $this->settings['whatsapp_numbers'] ) && ! empty( $this->settings['whatsapp_numbers'] ) ) {
foreach ( $this->settings['whatsapp_numbers'] as $number ) {
if ( ! empty( $number['number'] ) ) {
$has_valid_numbers = true;
break;
}
}
}
if ( ! $plugin_enabled ) {
echo '
';
esc_html_e( 'Click to Chat is disabled. WhatsApp buttons will not appear until you enable the plugin in Settings.', 'aicoso-click-to-chat' );
echo '
';
}
if ( ! $has_valid_numbers ) {
echo '
';
}
}
/**
* Render a plugin-native admin banner.
*
* @param string $message Banner message.
* @param string $type Banner type: info, success, warning, or error.
* @param bool $dismissible Whether the banner can be dismissed.
* @return void
*/
public function render_admin_banner( $message, $type = 'info', $dismissible = false ) {
$icons = array(
'info' => 'dashicons-info-outline',
'success' => 'dashicons-yes-alt',
'warning' => 'dashicons-warning',
'error' => 'dashicons-dismiss',
);
if ( ! isset( $icons[ $type ] ) ) {
$type = 'info';
}
$role = in_array( $type, array( 'warning', 'error' ), true ) ? 'alert' : 'status';
?>
render_admin_banner( $message['message'], $type, true );
}
}
/**
* Build breadcrumb args for a settings sub-page.
*
* @param string $section Settings section slug.
* @param string $config_tab Optional configuration sub-tab slug.
* @return array
*/
private function get_settings_breadcrumb_args( $section, $config_tab = 'general' ) {
$config_tab_labels = array(
'general' => esc_html__( 'General', 'aicoso-click-to-chat' ),
'button' => esc_html__( 'Button Style', 'aicoso-click-to-chat' ),
'display' => esc_html__( 'Display', 'aicoso-click-to-chat' ),
'exclusions' => esc_html__( 'Exclusions', 'aicoso-click-to-chat' ),
);
$args = array(
'breadcrumb_parent' => esc_html__( 'Settings', 'aicoso-click-to-chat' ),
'breadcrumb_parent_url' => $this->get_settings_hub_url( 'general' ),
);
if ( 'general' === $section ) {
$args['breadcrumb_current'] = isset( $config_tab_labels[ $config_tab ] ) ? $config_tab_labels[ $config_tab ] : $config_tab_labels['general'];
}
return $args;
}
/**
* Open the shared admin shell.
*
* @param array $args Shell arguments.
* @return void
*/
private function render_admin_shell_open( $args = array() ) {
$args = wp_parse_args(
$args,
array(
'breadcrumb_parent' => '',
'breadcrumb_parent_url' => '',
'breadcrumb_current' => '',
'intro' => '',
'show_setup_notices' => true,
'header_tabs' => array(),
'header_tab_current' => '',
)
);
?>
render_product_header(
array(
'breadcrumb_parent' => $args['breadcrumb_parent'],
'breadcrumb_parent_url' => $args['breadcrumb_parent_url'],
'breadcrumb_current' => $args['breadcrumb_current'],
'has_header_tabs' => ! empty( $args['header_tabs'] ),
)
);
if ( ! empty( $args['header_tabs'] ) ) {
$tab_aria = esc_html__( 'Settings sections', 'aicoso-click-to-chat' );
if ( esc_html__( 'Reports', 'aicoso-click-to-chat' ) === $args['breadcrumb_current'] ) {
$tab_aria = esc_html__( 'Report views', 'aicoso-click-to-chat' );
}
$this->render_admin_tabs(
$args['header_tabs'],
$args['header_tab_current'],
array(
'aria_label' => $tab_aria,
'class' => 'ctc-header-tabs',
)
);
}
?>
render_setup_status_notices(); ?>
get_menu_icon_url(),
58 // Position after WooCommerce.
);
add_submenu_page(
'click-to-chat',
esc_html__( 'Dashboard', 'aicoso-click-to-chat' ),
esc_html__( 'Dashboard', 'aicoso-click-to-chat' ),
'manage_options',
'click-to-chat',
array( $this, 'render_dashboard_page' )
);
add_submenu_page(
'click-to-chat',
esc_html__( 'Reports', 'aicoso-click-to-chat' ),
esc_html__( 'Reports', 'aicoso-click-to-chat' ),
'manage_options',
'click-to-chat-reports',
array( $this, 'render_reports_page' )
);
add_submenu_page(
'click-to-chat',
esc_html__( 'Settings', 'aicoso-click-to-chat' ),
esc_html__( 'Settings', 'aicoso-click-to-chat' ),
'manage_options',
'click-to-chat-settings',
array( $this, 'render_settings_page' )
);
}
/**
* Enqueue sidebar menu styles on every admin screen.
*
* @return void
*/
public function enqueue_admin_menu_assets() {
wp_enqueue_style(
'ctc-chat-admin-menu',
CTC_CHAT_PLUGIN_URL . 'admin/css/admin-menu.css',
array(),
CTC_CHAT_VERSION
);
}
/**
* Enqueue admin assets.
*
* @param string $hook Current admin page hook.
*/
public function enqueue_admin_assets( $hook ) {
// Only enqueue on plugin admin pages.
if ( strpos( $hook, 'click-to-chat' ) === false ) {
return;
}
// CSS.
wp_enqueue_style( 'wp-color-picker' );
// Enqueue Select2 from local files.
wp_enqueue_style(
'ctc-chat-select2',
CTC_CHAT_PLUGIN_URL . 'admin/lib/select2/select2.min.css',
array(),
'4.0.13'
);
wp_enqueue_style(
'ctc-chat-admin-styles',
CTC_CHAT_PLUGIN_URL . 'admin/css/admin.css',
array( 'wp-color-picker', 'ctc-chat-select2' ),
CTC_CHAT_VERSION
);
// JavaScript.
// Enqueue Select2 from local files.
wp_enqueue_script(
'ctc-chat-select2-js',
CTC_CHAT_PLUGIN_URL . 'admin/lib/select2/select2.min.js',
array( 'jquery' ),
'4.0.13',
true
);
wp_enqueue_script( 'wp-color-picker' );
wp_enqueue_script( 'jquery-ui-sortable' );
wp_enqueue_script(
'ctc-chat-admin-js',
CTC_CHAT_PLUGIN_URL . 'admin/js/admin.js',
array( 'jquery', 'wp-color-picker', 'ctc-chat-select2-js', 'jquery-ui-sortable' ),
CTC_CHAT_VERSION,
true
);
wp_localize_script(
'ctc-chat-admin-js',
'ctc_chat_admin',
array(
'ajaxurl' => admin_url( 'admin-ajax.php' ),
'ajax_url' => admin_url( 'admin-ajax.php' ),
'nonce' => wp_create_nonce( 'ctc_chat_admin_nonce' ),
'delete_number_confirm' => esc_html__( 'Are you sure you want to delete this WhatsApp number?', 'aicoso-click-to-chat' ),
'duplicate_name_error' => esc_html__( 'This name is already being used. Please choose a different name.', 'aicoso-click-to-chat' ),
'loading_text' => esc_html__( 'Loading...', 'aicoso-click-to-chat' ),
'preview_text' => esc_html__( 'Preview', 'aicoso-click-to-chat' ),
'default_button_text' => esc_html__( 'Order via WhatsApp', 'aicoso-click-to-chat' ),
'copy_success' => esc_html__( 'Shortcode copied to clipboard!', 'aicoso-click-to-chat' ),
'copy_error' => esc_html__( 'Failed to copy shortcode. Please select and copy manually.', 'aicoso-click-to-chat' ),
'select_products_text' => esc_html__( 'Select products...', 'aicoso-click-to-chat' ),
'select_categories_text' => esc_html__( 'Select categories...', 'aicoso-click-to-chat' ),
'select_pages_text' => esc_html__( 'Select pages...', 'aicoso-click-to-chat' ),
'i18n' => array(
'confirm_delete' => esc_html__( 'Are you sure you want to delete this WhatsApp number?', 'aicoso-click-to-chat' ),
'number_required' => esc_html__( 'WhatsApp number is required.', 'aicoso-click-to-chat' ),
'name_required' => esc_html__( 'Name is required.', 'aicoso-click-to-chat' ),
'select_products' => esc_html__( 'Select products...', 'aicoso-click-to-chat' ),
'select_categories' => esc_html__( 'Select categories...', 'aicoso-click-to-chat' ),
'select_pages' => esc_html__( 'Select pages...', 'aicoso-click-to-chat' ),
),
)
);
$this->enqueue_analytics_assets( $hook );
}
/**
* Enqueue dashboard and reports analytics assets.
*
* @param string $hook Admin page hook.
*/
private function enqueue_analytics_assets( $hook ) {
$analytics_hooks = array(
'toplevel_page_click-to-chat',
'click-to-chat_page_click-to-chat-reports',
);
if ( ! in_array( $hook, $analytics_hooks, true ) ) {
return;
}
$currency = function_exists( 'get_woocommerce_currency_symbol' ) ? get_woocommerce_currency_symbol() : '$';
wp_enqueue_script(
'ctc-chat-analytics-shared',
CTC_CHAT_PLUGIN_URL . 'admin/js/analytics-shared.js',
array( 'jquery' ),
CTC_CHAT_VERSION,
true
);
wp_localize_script(
'ctc-chat-analytics-shared',
'ctc_chat_analytics',
array(
'ajaxurl' => admin_url( 'admin-ajax.php' ),
'nonce' => wp_create_nonce( 'ctc_chat_admin_nonce' ),
'currency' => $currency,
'i18n' => array(
'loading' => __( 'Loadingβ¦', 'aicoso-click-to-chat' ),
'error' => __( 'Could not load analytics data.', 'aicoso-click-to-chat' ),
'empty' => __( 'No WhatsApp clicks in this period.', 'aicoso-click-to-chat' ),
'unavailable' => __( 'Data unavailable', 'aicoso-click-to-chat' ),
'disabled' => __( 'Click tracking is disabled in Settings.', 'aicoso-click-to-chat' ),
'clicks' => __( 'clicks', 'aicoso-click-to-chat' ),
'about' => __( 'About', 'aicoso-click-to-chat' ),
'all_numbers' => __( 'All numbers', 'aicoso-click-to-chat' ),
'unattributed' => __( 'Unattributed', 'aicoso-click-to-chat' ),
'number_filter' => __( 'WhatsApp number', 'aicoso-click-to-chat' ),
'filter_unavailable' => __( 'The selected WhatsApp number is no longer available. Showing all numbers.', 'aicoso-click-to-chat' ),
'filter_retry_error' => __( 'The dashboard could not refresh after changing the number filter. Please try again.', 'aicoso-click-to-chat' ),
'kpi_help' => array(
'whatsapp_clicks' => __( 'Total WhatsApp button clicks recorded during the selected period. Repeated clicks are included.', 'aicoso-click-to-chat' ),
'unique_clicks' => __( 'Clicks counted once per visitor, placement, product, and order within the 24-hour deduplication window.', 'aicoso-click-to-chat' ),
'high_intent_clicks' => __( 'WhatsApp clicks from Cart, Checkout, and Thank You pages during the selected period.', 'aicoso-click-to-chat' ),
'cart_value_clicked' => __( 'Sum of cart totals captured when visitors clicked WhatsApp. This is not revenue or an average.', 'aicoso-click-to-chat' ),
'mobile_share' => __( 'Percentage of clicks from mobile devices among clicks with a recognized device type. When device data is unavailable, this metric is unavailable.', 'aicoso-click-to-chat' ),
'top_placement' => __( 'The button placement with the most WhatsApp clicks during the selected period.', 'aicoso-click-to-chat' ),
),
),
'urls' => array(
'settings' => admin_url( 'admin.php?page=click-to-chat-settings' ),
'reports' => admin_url( 'admin.php?page=click-to-chat-reports' ),
'numbers' => $this->get_settings_hub_url( 'numbers' ),
),
)
);
if ( 'toplevel_page_click-to-chat' === $hook ) {
wp_enqueue_script(
'ctc-chat-analytics-dashboard',
CTC_CHAT_PLUGIN_URL . 'admin/js/analytics-dashboard.js',
array( 'ctc-chat-analytics-shared' ),
CTC_CHAT_VERSION,
true
);
}
if ( 'click-to-chat_page_click-to-chat-reports' === $hook ) {
wp_enqueue_script(
'ctc-chat-analytics-reports',
CTC_CHAT_PLUGIN_URL . 'admin/js/analytics-reports.js',
array( 'ctc-chat-analytics-shared' ),
CTC_CHAT_VERSION,
true
);
}
}
/**
* Add action links to the plugins page.
*
* @param array $links Plugin action links.
* @return array Modified action links.
*/
public function add_action_links( $links ) {
$plugin_links = array(
'
' . esc_html__( 'Settings', 'aicoso-click-to-chat' ) . '',
);
return array_merge( $plugin_links, $links );
}
/**
* Render the dashboard page.
*/
public function render_dashboard_page() {
if ( ! current_user_can( 'manage_options' ) ) {
return;
}
$this->render_admin_shell_open(
array(
'breadcrumb_current' => esc_html__( 'Dashboard', 'aicoso-click-to-chat' ),
'show_setup_notices' => false,
)
);
include CTC_CHAT_PLUGIN_DIR . 'admin/views/dashboard-page.php';
$this->render_admin_shell_close();
}
/**
* Render the reports page.
*/
public function render_reports_page() {
if ( ! current_user_can( 'manage_options' ) ) {
return;
}
$current_report = $this->get_current_report();
$this->render_admin_shell_open(
array(
'breadcrumb_current' => esc_html__( 'Reports', 'aicoso-click-to-chat' ),
'intro' => esc_html__( 'Drill into WhatsApp click data by placement, product, number, and page.', 'aicoso-click-to-chat' ),
'show_setup_notices' => false,
'header_tabs' => $this->get_reports_header_tabs(),
'header_tab_current' => $current_report,
)
);
include CTC_CHAT_PLUGIN_DIR . 'admin/views/reports-page.php';
$this->render_admin_shell_close();
}
/**
* Render the unified settings hub.
*/
public function render_settings_page() {
if ( ! current_user_can( 'manage_options' ) ) {
return;
}
if ( self::$settings_page_rendered ) {
return;
}
self::$settings_page_rendered = true;
$current_section = $this->get_current_settings_section();
$current_tab = $this->get_current_settings_tab();
if ( isset( $_POST['ctc_chat_save_settings'] ) && check_admin_referer( 'ctc_chat_settings_nonce', 'ctc_chat_settings_nonce' ) ) {
$this->save_settings();
}
if ( isset( $_POST['ctc_chat_save_numbers'] ) && check_admin_referer( 'ctc_chat_numbers_nonce', 'ctc_chat_numbers_nonce' ) ) {
$this->save_numbers();
}
if ( isset( $_POST['ctc_chat_save_templates'] ) && check_admin_referer( 'ctc_chat_templates_nonce', 'ctc_chat_templates_nonce' ) ) {
$this->save_templates();
}
$section_intros = array(
'general' => esc_html__( 'Configure WhatsApp buttons, placement, exclusions, and catalog mode.', 'aicoso-click-to-chat' ),
'numbers' => esc_html__( 'Add WhatsApp numbers and assign them to products, categories, or pages.', 'aicoso-click-to-chat' ),
'templates' => esc_html__( 'Customize pre-filled WhatsApp messages for products, cart, checkout, and floating buttons.', 'aicoso-click-to-chat' ),
'shortcodes' => esc_html__( 'Build a custom WhatsApp button shortcode and copy it into any page or post.', 'aicoso-click-to-chat' ),
);
$breadcrumb_args = $this->get_settings_breadcrumb_args( $current_section, $current_tab );
$this->render_admin_shell_open(
array_merge(
$breadcrumb_args,
array(
'intro' => $section_intros[ $current_section ],
'header_tabs' => $this->get_settings_header_tabs(),
'header_tab_current' => $current_section,
)
)
);
include CTC_CHAT_PLUGIN_DIR . 'admin/views/settings-hub-page.php';
$this->render_admin_shell_close();
}
/**
* Save general settings.
*/
private function save_settings() {
// Get existing settings.
$settings = get_option( 'ctc_chat_settings', array() );
// Nonce is verified in render_settings_page() before calling this method.
// phpcs:disable WordPress.Security.NonceVerification.Missing
// Get the current tab for redirect.
$current_tab = isset( $_POST['ctc_chat_current_tab'] ) ? sanitize_text_field( wp_unslash( $_POST['ctc_chat_current_tab'] ) ) : 'general';
// Save plugin enabled status.
$settings['plugin_enabled'] = isset( $_POST['ctc_chat_plugin_enabled'] ) ? true : false;
// Sanitize and update button settings.
if ( isset( $_POST['ctc_chat_button'] ) && is_array( $_POST['ctc_chat_button'] ) ) {
// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Each field is sanitized individually below.
$ctc_button = wp_unslash( $_POST['ctc_chat_button'] );
$button_settings = array(
'text' => isset( $ctc_button['text'] ) ? sanitize_text_field( $ctc_button['text'] ) : '',
'icon' => isset( $_POST['ctc_chat_button']['icon'] ) ? true : false,
'bg_color' => isset( $ctc_button['bg_color'] ) ? sanitize_hex_color( $ctc_button['bg_color'] ) : '#25D366',
'text_color' => isset( $ctc_button['text_color'] ) ? sanitize_hex_color( $ctc_button['text_color'] ) : '#ffffff',
);
$settings['button_settings'] = $button_settings;
}
// Sanitize and update single product settings.
if ( isset( $_POST['ctc_chat_single_product'] ) && is_array( $_POST['ctc_chat_single_product'] ) ) {
// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Each field is sanitized individually below.
$ctc_single_product = wp_unslash( $_POST['ctc_chat_single_product'] );
$single_product = array(
'enabled' => isset( $_POST['ctc_chat_single_product']['enabled'] ) ? true : false,
'position' => isset( $ctc_single_product['position'] ) ? sanitize_text_field( $ctc_single_product['position'] ) : 'after_add_to_cart',
);
$settings['single_product'] = $single_product;
}
// Sanitize and update shop page settings.
if ( isset( $_POST['ctc_chat_shop_page'] ) && is_array( $_POST['ctc_chat_shop_page'] ) ) {
// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Each field is sanitized individually below.
$ctc_shop_page = wp_unslash( $_POST['ctc_chat_shop_page'] );
$shop_page = array(
'enabled' => isset( $_POST['ctc_chat_shop_page']['enabled'] ) ? true : false,
'position' => isset( $ctc_shop_page['position'] ) ? sanitize_text_field( $ctc_shop_page['position'] ) : 'after_add_to_cart',
);
$settings['shop_page'] = $shop_page;
}
// Sanitize and update cart page settings.
$ctc_cart_page_position = isset( $_POST['ctc_chat_cart_page']['position'] ) ? sanitize_text_field( wp_unslash( $_POST['ctc_chat_cart_page']['position'] ) ) : 'after_cart_table';
$settings['cart_page'] = array(
'enabled' => isset( $_POST['ctc_chat_cart_page']['enabled'] ) ? true : false,
'position' => $ctc_cart_page_position,
);
// Sanitize and update checkout page settings.
$ctc_checkout_page_position = isset( $_POST['ctc_chat_checkout_page']['position'] ) ? sanitize_text_field( wp_unslash( $_POST['ctc_chat_checkout_page']['position'] ) ) : 'after_payment';
$settings['checkout_page'] = array(
'enabled' => isset( $_POST['ctc_chat_checkout_page']['enabled'] ) ? true : false,
'position' => $ctc_checkout_page_position,
);
// Sanitize and update thank you / order tracking page settings.
// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Fields sanitized individually below.
$thankyou_post = isset( $_POST['ctc_chat_thankyou_page'] ) && is_array( $_POST['ctc_chat_thankyou_page'] ) ? wp_unslash( $_POST['ctc_chat_thankyou_page'] ) : array();
$settings['thankyou_page'] = array(
'enabled' => isset( $thankyou_post['enabled'] ) ? true : false,
'my_account_orders' => isset( $thankyou_post['my_account_orders'] ) ? true : false,
'my_account_view_order' => isset( $thankyou_post['my_account_view_order'] ) ? true : false,
'button_text' => ! empty( $thankyou_post['button_text'] ) ? sanitize_text_field( $thankyou_post['button_text'] ) : esc_html__( 'Track My Order on WhatsApp π', 'aicoso-click-to-chat' ),
);
// Sanitize and update cart/checkout abandonment nudge settings.
if ( isset( $_POST['ctc_chat_cart_checkout_nudge'] ) && is_array( $_POST['ctc_chat_cart_checkout_nudge'] ) ) {
// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Fields sanitized individually below.
$nudge_post = wp_unslash( $_POST['ctc_chat_cart_checkout_nudge'] );
$settings['cart_checkout_nudge'] = array(
'enabled' => isset( $nudge_post['enabled'] ) ? true : false,
'trigger' => isset( $nudge_post['trigger'] ) && in_array( $nudge_post['trigger'], array( 'inactivity', 'exit_intent', 'both' ), true ) ? $nudge_post['trigger'] : 'both',
'delay' => isset( $nudge_post['delay'] ) ? max( 3, min( 300, absint( $nudge_post['delay'] ) ) ) : 20,
'frequency' => isset( $nudge_post['frequency'] ) && in_array( $nudge_post['frequency'], array( 'reappear', 'once_per_session' ), true ) ? $nudge_post['frequency'] : 'reappear',
'title' => isset( $nudge_post['title'] ) ? sanitize_text_field( $nudge_post['title'] ) : esc_html__( 'Need help with your order?', 'aicoso-click-to-chat' ),
'message' => isset( $nudge_post['message'] ) ? sanitize_textarea_field( $nudge_post['message'] ) : esc_html__( 'Have questions about payment, shipping, or need assistance? Chat with us on WhatsApp!', 'aicoso-click-to-chat' ),
'button_text' => isset( $nudge_post['button_text'] ) ? sanitize_text_field( $nudge_post['button_text'] ) : esc_html__( 'Chat with Support π¬', 'aicoso-click-to-chat' ),
);
}
// Sanitize and update back in stock alert settings.
if ( isset( $_POST['ctc_chat_back_in_stock'] ) && is_array( $_POST['ctc_chat_back_in_stock'] ) ) {
// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Fields sanitized individually below.
$stock_post = wp_unslash( $_POST['ctc_chat_back_in_stock'] );
$settings['back_in_stock'] = array(
'enabled' => isset( $stock_post['enabled'] ) ? true : false,
'button_text' => isset( $stock_post['button_text'] ) ? sanitize_text_field( $stock_post['button_text'] ) : esc_html__( 'Notify Me on WhatsApp π', 'aicoso-click-to-chat' ),
'bg_color' => isset( $stock_post['bg_color'] ) && sanitize_hex_color( $stock_post['bg_color'] ) ? sanitize_hex_color( $stock_post['bg_color'] ) : '#ff9800',
'text_color' => isset( $stock_post['text_color'] ) && sanitize_hex_color( $stock_post['text_color'] ) ? sanitize_hex_color( $stock_post['text_color'] ) : '#ffffff',
'message' => isset( $stock_post['message'] ) ? sanitize_textarea_field( $stock_post['message'] ) : esc_html__( "Hello! I noticed that *{product_name}* (SKU: {product_sku}) is currently out of stock.\n\nPlease notify me via WhatsApp as soon as it is back in stock!\nLink: {product_url}", 'aicoso-click-to-chat' ),
);
}
// Sanitize and update coupon engine settings.
if ( isset( $_POST['ctc_chat_coupon_engine'] ) && is_array( $_POST['ctc_chat_coupon_engine'] ) ) {
// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Fields sanitized individually below.
$coupon_post = wp_unslash( $_POST['ctc_chat_coupon_engine'] );
$settings['coupon_engine'] = array(
'enabled' => isset( $coupon_post['enabled'] ) ? true : false,
'coupon_code' => isset( $coupon_post['coupon_code'] ) ? sanitize_text_field( $coupon_post['coupon_code'] ) : '',
'custom_discount' => isset( $coupon_post['custom_discount'] ) ? sanitize_text_field( $coupon_post['custom_discount'] ) : '',
'badge_text' => isset( $coupon_post['badge_text'] ) ? sanitize_text_field( $coupon_post['badge_text'] ) : esc_html__( 'π Chat to get 10% OFF!', 'aicoso-click-to-chat' ),
'badge_bg' => isset( $coupon_post['badge_bg'] ) && sanitize_hex_color( $coupon_post['badge_bg'] ) ? sanitize_hex_color( $coupon_post['badge_bg'] ) : '#e11d48',
'badge_color' => isset( $coupon_post['badge_color'] ) && sanitize_hex_color( $coupon_post['badge_color'] ) ? sanitize_hex_color( $coupon_post['badge_color'] ) : '#ffffff',
'show_on_floating' => isset( $coupon_post['show_on_floating'] ) ? true : false,
'show_on_product' => isset( $coupon_post['show_on_product'] ) ? true : false,
'message' => isset( $coupon_post['message'] ) ? sanitize_textarea_field( $coupon_post['message'] ) : esc_html__( "π *Special Discount Claim*\n\nHello! I'd like to claim my discount coupon: *{coupon_code}* ({discount_amount})\n\n*Product:* {product_name}\n*Page:* {current_page_url}\n\nCan you please assist me with applying this discount to my order? Thank you!", 'aicoso-click-to-chat' ),
);
}
// Sanitize and update floating button settings.
if ( isset( $_POST['ctc_chat_floating_button'] ) && is_array( $_POST['ctc_chat_floating_button'] ) ) {
// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Each field is sanitized individually below.
$ctc_floating_button = wp_unslash( $_POST['ctc_chat_floating_button'] );
$floating_button = array(
'enabled' => isset( $_POST['ctc_chat_floating_button']['enabled'] ) ? true : false,
'position' => isset( $ctc_floating_button['position'] ) ? sanitize_text_field( $ctc_floating_button['position'] ) : 'bottom_right',
);
$settings['floating_button'] = $floating_button;
}
// Sanitize and update exclusions.
// Always reset exclusions to ensure removed items are cleared.
$exclusions = array(
'pages' => array(),
'posts' => array(),
'categories' => array(),
'tags' => array(),
'products' => array(),
);
if ( isset( $_POST['ctc_chat_exclusions'] ) && is_array( $_POST['ctc_chat_exclusions'] ) ) {
// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Each field is sanitized individually below.
$ctc_exclusions = wp_unslash( $_POST['ctc_chat_exclusions'] );
// Pages.
if ( isset( $ctc_exclusions['pages'] ) && is_array( $ctc_exclusions['pages'] ) ) {
foreach ( $ctc_exclusions['pages'] as $page_id ) {
$exclusions['pages'][] = absint( $page_id );
}
}
// Posts.
if ( isset( $ctc_exclusions['posts'] ) && is_array( $ctc_exclusions['posts'] ) ) {
foreach ( $ctc_exclusions['posts'] as $post_id ) {
$exclusions['posts'][] = absint( $post_id );
}
}
// Categories.
if ( isset( $ctc_exclusions['categories'] ) && is_array( $ctc_exclusions['categories'] ) ) {
foreach ( $ctc_exclusions['categories'] as $category_id ) {
$exclusions['categories'][] = absint( $category_id );
}
}
// Tags.
if ( isset( $ctc_exclusions['tags'] ) && is_array( $ctc_exclusions['tags'] ) ) {
foreach ( $ctc_exclusions['tags'] as $tag_id ) {
$exclusions['tags'][] = absint( $tag_id );
}
}
// Products.
if ( isset( $ctc_exclusions['products'] ) && is_array( $ctc_exclusions['products'] ) ) {
foreach ( $ctc_exclusions['products'] as $product_id ) {
$exclusions['products'][] = absint( $product_id );
}
}
}
$settings['exclusions'] = $exclusions;
// Sanitize and update advanced settings.
$catalog_mode = isset( $_POST['ctc_chat_advanced']['catalog_mode'] ) ? true : false;
$custom_css = isset( $_POST['ctc_chat_advanced']['custom_css'] ) ? wp_strip_all_tags( wp_unslash( $_POST['ctc_chat_advanced']['custom_css'] ) ) : '';
// If catalog mode is enabled, force all hide options to be true.
if ( $catalog_mode ) {
$advanced = array(
'hide_add_to_cart' => true,
'hide_proceed_checkout' => true,
'hide_place_order' => true,
'catalog_mode' => true,
'custom_css' => $custom_css,
);
} else {
// Otherwise, check individual options.
$advanced = array(
'hide_add_to_cart' => isset( $_POST['ctc_chat_advanced']['hide_add_to_cart'] ) ? true : false,
'hide_proceed_checkout' => isset( $_POST['ctc_chat_advanced']['hide_proceed_checkout'] ) ? true : false,
'hide_place_order' => isset( $_POST['ctc_chat_advanced']['hide_place_order'] ) ? true : false,
'catalog_mode' => false,
'custom_css' => $custom_css,
);
}
$settings['advanced'] = $advanced;
// Sanitize and update desktop QR modal settings.
if ( isset( $_POST['ctc_chat_qr_modal'] ) && is_array( $_POST['ctc_chat_qr_modal'] ) ) {
// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Fields sanitized individually below.
$qr_post = wp_unslash( $_POST['ctc_chat_qr_modal'] );
$settings['qr_modal'] = array(
'enabled' => isset( $qr_post['enabled'] ) ? true : false,
'title' => isset( $qr_post['title'] ) ? sanitize_text_field( $qr_post['title'] ) : esc_html__( 'Scan to Chat on WhatsApp', 'aicoso-click-to-chat' ),
'description' => isset( $qr_post['description'] ) ? sanitize_textarea_field( $qr_post['description'] ) : esc_html__( 'Point your phone camera or WhatsApp QR scanner at this code to start chatting instantly.', 'aicoso-click-to-chat' ),
'show_web_link' => isset( $qr_post['show_web_link'] ) ? true : false,
);
}
// Sanitize and update GDPR & Privacy Compliance settings.
if ( isset( $_POST['ctc_chat_privacy_compliance'] ) && is_array( $_POST['ctc_chat_privacy_compliance'] ) ) {
// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Fields sanitized individually below.
$privacy_post = wp_unslash( $_POST['ctc_chat_privacy_compliance'] );
$consent_mode = isset( $privacy_post['consent_mode'] ) && in_array( $privacy_post['consent_mode'], array( 'prompt', 'inline_notice' ), true )
? $privacy_post['consent_mode']
: 'prompt';
$settings['privacy_compliance'] = array(
'enabled' => isset( $privacy_post['enabled'] ),
'consent_mode' => $consent_mode,
'notice_text' => isset( $privacy_post['notice_text'] ) ? sanitize_textarea_field( $privacy_post['notice_text'] ) : '',
'link_text' => isset( $privacy_post['link_text'] ) ? sanitize_text_field( $privacy_post['link_text'] ) : esc_html__( 'Privacy Policy', 'aicoso-click-to-chat' ),
'custom_policy_url' => isset( $privacy_post['custom_policy_url'] ) ? esc_url_raw( trim( $privacy_post['custom_policy_url'] ) ) : '',
'anonymize_ip' => isset( $privacy_post['anonymize_ip'] ),
'agree_button' => isset( $privacy_post['agree_button'] ) ? sanitize_text_field( $privacy_post['agree_button'] ) : esc_html__( 'Accept & Chat', 'aicoso-click-to-chat' ),
'cancel_button' => isset( $privacy_post['cancel_button'] ) ? sanitize_text_field( $privacy_post['cancel_button'] ) : esc_html__( 'Cancel', 'aicoso-click-to-chat' ),
);
}
$retention_days = isset( $_POST['ctc_chat_analytics']['retention_days'] ) ? absint( $_POST['ctc_chat_analytics']['retention_days'] ) : 365;
$retention_days = max( 30, min( 730, $retention_days ) );
$settings['analytics'] = array(
'enabled' => isset( $_POST['ctc_chat_analytics']['enabled'] ),
'retention_days' => $retention_days,
'track_ip' => isset( $_POST['ctc_chat_analytics']['track_ip'] ),
'dedupe_hours' => 24,
'exclude_bots' => isset( $_POST['ctc_chat_analytics']['exclude_bots'] ),
'visitor_cookie_ttl' => isset( $settings['analytics']['visitor_cookie_ttl'] ) ? absint( $settings['analytics']['visitor_cookie_ttl'] ) : 30,
);
// Update settings.
update_option( 'ctc_chat_settings', $settings );
// phpcs:enable WordPress.Security.NonceVerification.Missing
// Store success message in transient (will be displayed after redirect).
set_transient( 'ctc_chat_settings_message', 'success', 30 );
// Redirect to the same tab.
$redirect_url = $this->get_settings_hub_url( 'general', $current_tab );
wp_safe_redirect( $redirect_url );
exit;
}
/**
* Save WhatsApp numbers.
*/
private function save_numbers() {
// Nonce is verified in render_numbers_page() before calling this method.
// phpcs:disable WordPress.Security.NonceVerification.Missing
// Get existing settings.
$settings = get_option( 'ctc_chat_settings', array() );
// Initialize empty array for WhatsApp numbers.
$whatsapp_numbers = array();
// Process deleted numbers.
$deleted_numbers = array();
if ( isset( $_POST['ctc_chat_deleted_numbers'] ) && ! empty( $_POST['ctc_chat_deleted_numbers'] ) ) {
$deleted_numbers = array_map( 'absint', explode( ',', sanitize_text_field( wp_unslash( $_POST['ctc_chat_deleted_numbers'] ) ) ) );
}
// Check for duplicate names.
$number_names = array();
$has_duplicate = false;
// Process submitted numbers.
if ( isset( $_POST['ctc_numbers'] ) && is_array( $_POST['ctc_numbers'] ) ) {
// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Each field is sanitized individually below.
$ctc_numbers = ctc_chat_normalize_number_record_ids( wp_unslash( $_POST['ctc_numbers'] ) );
foreach ( $ctc_numbers as $number_data ) {
// Get the number ID.
$number_id = isset( $number_data['id'] ) ? absint( $number_data['id'] ) : 0;
// Skip this number if it was deleted.
if ( in_array( $number_id, $deleted_numbers, true ) ) {
continue;
}
// Check for duplicate names.
$name = isset( $number_data['name'] ) ? sanitize_text_field( $number_data['name'] ) : '';
if ( in_array( $name, $number_names, true ) ) {
// We found a duplicate name.
$has_duplicate = true;
continue; // Skip this number.
}
// Add name to our tracking array.
if ( ! empty( $name ) ) {
$number_names[] = $name;
}
$assignments = array(
'products' => array(),
'categories' => array(),
'pages' => array(),
);
// Process product assignments.
if ( isset( $number_data['assignments']['products'] ) && is_array( $number_data['assignments']['products'] ) ) {
foreach ( $number_data['assignments']['products'] as $product_id ) {
$assignments['products'][] = absint( $product_id );
}
}
// Process category assignments.
if ( isset( $number_data['assignments']['categories'] ) && is_array( $number_data['assignments']['categories'] ) ) {
foreach ( $number_data['assignments']['categories'] as $category_id ) {
$assignments['categories'][] = absint( $category_id );
}
}
// Process page assignments.
if ( isset( $number_data['assignments']['pages'] ) && is_array( $number_data['assignments']['pages'] ) ) {
foreach ( $number_data['assignments']['pages'] as $page_id ) {
$assignments['pages'][] = absint( $page_id );
}
}
// Add sanitized number data to the array.
$whatsapp_numbers[] = array(
'id' => $number_id,
'name' => isset( $number_data['name'] ) ? sanitize_text_field( $number_data['name'] ) : '',
'number' => isset( $number_data['number'] ) ? sanitize_text_field( $number_data['number'] ) : '',
'description' => isset( $number_data['description'] ) ? sanitize_textarea_field( $number_data['description'] ) : '',
'is_default' => ! empty( $number_data['is_default'] ),
'assignments' => $assignments,
);
}
}
// Update WhatsApp numbers in settings.
$settings['whatsapp_numbers'] = $whatsapp_numbers;
// Update settings.
update_option( 'ctc_chat_settings', $settings );
// Add message based on whether we found duplicates.
if ( $has_duplicate ) {
add_settings_error(
'ctc_numbers',
'ctc_numbers_duplicates',
esc_html__( 'Some WhatsApp numbers were not saved because they had duplicate names. Each WhatsApp number must have a unique name.', 'aicoso-click-to-chat' ),
'error'
);
} else {
add_settings_error(
'ctc_numbers',
'ctc_numbers_updated',
esc_html__( 'WhatsApp numbers saved successfully.', 'aicoso-click-to-chat' ),
'updated'
);
}
// phpcs:enable WordPress.Security.NonceVerification.Missing
}
/**
* Save message templates.
*/
private function save_templates() {
// Nonce is verified in render_templates_page() before calling this method.
// phpcs:disable WordPress.Security.NonceVerification.Missing
// Get existing settings.
$settings = get_option( 'ctc_chat_settings', array() );
// Process submitted templates.
if ( isset( $_POST['ctc_chat_templates'] ) && is_array( $_POST['ctc_chat_templates'] ) ) {
// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Each field is sanitized individually below.
$ctc_templates = wp_unslash( $_POST['ctc_chat_templates'] );
$existing_templates = isset( $settings['message_templates'] ) && is_array( $settings['message_templates'] ) ? $settings['message_templates'] : array();
$message_templates = array(
'single_product' => isset( $ctc_templates['single_product'] ) ? sanitize_textarea_field( $ctc_templates['single_product'] ) : ( $existing_templates['single_product'] ?? '' ),
'shop' => isset( $ctc_templates['shop'] ) ? sanitize_textarea_field( $ctc_templates['shop'] ) : ( $existing_templates['shop'] ?? '' ),
'cart_checkout' => isset( $ctc_templates['cart_checkout'] ) ? sanitize_textarea_field( $ctc_templates['cart_checkout'] ) : ( $existing_templates['cart_checkout'] ?? '' ),
'thank_you' => isset( $ctc_templates['thank_you'] ) ? sanitize_textarea_field( $ctc_templates['thank_you'] ) : ( $existing_templates['thank_you'] ?? '' ),
'floating' => isset( $ctc_templates['floating'] ) ? sanitize_textarea_field( $ctc_templates['floating'] ) : ( $existing_templates['floating'] ?? '' ),
'variations' => isset( $ctc_templates['variations'] ) ? sanitize_textarea_field( $ctc_templates['variations'] ) : ( $existing_templates['variations'] ?? '' ),
);
// Update message templates in settings.
$settings['message_templates'] = $message_templates;
// Update settings.
update_option( 'ctc_chat_settings', $settings );
// Add success message.
add_settings_error(
'ctc_templates',
'ctc_templates_updated',
esc_html__( 'Message templates saved successfully.', 'aicoso-click-to-chat' ),
'updated'
);
}
// phpcs:enable WordPress.Security.NonceVerification.Missing
}
/**
* Add meta boxes to product edit screen.
*/
public function add_product_meta_boxes() {
add_meta_box(
'ctc_chat_product_settings',
esc_html__( 'WhatsApp Shopping Settings', 'aicoso-click-to-chat' ),
array( $this, 'render_product_meta_box' ),
'product',
'side',
'default'
);
}
/**
* Render meta box on product edit screen.
*
* @param WP_Post $post Current post object.
*/
public function render_product_meta_box( $post ) {
// Add nonce for security.
wp_nonce_field( 'ctc_chat_product_meta_nonce', 'ctc_chat_product_meta_nonce' );
// Get current values.
$hide_button = get_post_meta( $post->ID, '_ctc_chat_hide_button', true );
$custom_message = get_post_meta( $post->ID, '_ctc_chat_custom_message', true );
$assigned_number = get_post_meta( $post->ID, '_ctc_chat_assigned_number', true );
// Get available WhatsApp numbers.
$whatsapp_numbers = isset( $this->settings['whatsapp_numbers'] ) ? $this->settings['whatsapp_numbers'] : array();
?>
'shop_coupon',
'post_status' => 'publish',
'posts_per_page' => 100,
'orderby' => 'title',
'order' => 'ASC',
)
);
$list = array();
foreach ( $coupons as $post ) {
$code = $post->post_title;
$discount_desc = '';
if ( class_exists( 'WC_Coupon' ) ) {
try {
$coupon = new WC_Coupon( $code );
if ( $coupon->get_id() ) {
$amount = $coupon->get_amount();
$type = $coupon->get_discount_type();
if ( 'percent' === $type ) {
$discount_desc = $amount . '% OFF';
} elseif ( 'fixed_cart' === $type || 'fixed_product' === $type ) {
$discount_desc = function_exists( 'wc_price' ) ? wp_strip_all_tags( wc_price( $amount ) ) . ' OFF' : '$' . $amount . ' OFF';
} else {
$discount_desc = $amount;
}
}
} catch ( Exception $e ) {
unset( $e );
}
}
$list[] = array(
'code' => $code,
'description' => $discount_desc,
);
}
return $list;
}
}