' . '' . '' ); $failed = self::count_recent_failures(); $menu_label = '6rsh' . ( $failed > 0 ? ' ' . min( $failed, 99 ) . '' : '' ); add_menu_page( __( '6rsh WhatsApp', '6rsh-messaging-notifications' ), wp_kses_post( $menu_label ), 'manage_options', '6rsh-messaging-notifications', array( __CLASS__, 'render_page' ), $icon, 58 ); } /** * Fetch the current plan LIVE on every load of the 6rsh settings screen. * * This is what makes a plan change visible on plain page reload with no * button press: the whole page renders from this result. Failure is soft — * live() falls back to the last-known snapshot, so a paid store stays * unlocked when the network hiccups. * * @return void */ public static function prime_entitlements_on_page_load(): void { try { SixRsh_Entitlements::prime_for_admin_page(); } catch ( \Throwable $e ) { error_log( '[6rsh WhatsApp] prime_entitlements_on_page_load exception: ' . $e->getMessage() ); } } /** * Render the main plugin admin page (wizard or settings). * * @return void */ public static function render_page(): void { try { // phpcs:ignore WordPress.Security.NonceVerification.Recommended $is_setup = ! empty( $_GET['setup'] ); $complete = (bool) get_option( 'sixrsh_wizard_complete' ); if ( $is_setup && ! $complete ) { include SIXRSH_PATH . 'admin/wizard.php'; } else { include SIXRSH_PATH . 'admin/settings.php'; } } catch ( \Throwable $e ) { error_log( '[6rsh WhatsApp] Admin::render_page exception: ' . $e->getMessage() ); echo '

' . esc_html__( '6rsh WhatsApp encountered an error loading this page.', '6rsh-messaging-notifications' ) . '

'; } } /** * Enqueue admin CSS and JS on the plugin page. * * @param string $hook Current admin page hook suffix. * * @return void */ public static function enqueue_assets( string $hook ): void { if ( 'toplevel_page_6rsh-messaging-notifications' !== $hook ) { return; } try { wp_enqueue_style( 'sixrsh-admin-css', SIXRSH_URL . 'admin/assets/admin.css', array(), SIXRSH_VERSION ); wp_enqueue_script( 'sixrsh-admin-js', SIXRSH_URL . 'admin/assets/admin.js', array(), SIXRSH_VERSION, true ); $settings = get_option( 'sixrsh_settings', array() ); $account = get_option( 'sixrsh_connected_account', array() ); $templates = get_option( 'sixrsh_available_templates', array() ); $mapping = get_option( 'sixrsh_template_mapping', array() ); // Build per-event variable mappings for JS. $ev_slugs = array_merge( array_keys( self::default_settings()['events'] ), function_exists( 'wc_get_order_statuses' ) ? array_keys( wc_get_order_statuses() ) : array() ); $event_variables_js = array(); foreach ( $ev_slugs as $s ) { $raw = get_option( 'sixrsh_event_' . $s . '_variables', '' ); $event_variables_js[ $s ] = $raw ? json_decode( $raw, true ) : null; } wp_localize_script( 'sixrsh-admin-js', 'sixrshAdmin', array( 'ajax_url' => admin_url( 'admin-ajax.php' ), 'nonce' => wp_create_nonce( 'sixrsh_admin' ), 'restUrl' => rest_url( '6rsh/v1' ), 'restNonce' => wp_create_nonce( 'wp_rest' ), 'settings' => $settings, 'account' => $account, 'templates' => $templates, 'mapping' => $mapping, 'wizard_complete' => (bool) get_option( 'sixrsh_wizard_complete' ), 'settings_url' => admin_url( 'admin.php?page=6rsh-messaging-notifications' ), 'currentTab' => sanitize_key( $_GET['tab'] ?? 'connection' ), 'entitlements' => array( 'abandoned_carts' => SixRsh_Entitlements::can_use_abandoned_carts(), 'notifications_limit' => SixRsh_Entitlements::notifications_limit(), 'enabled_count' => SixRsh_Entitlements::enabled_notification_count(), 'grandfathered_until' => SixRsh_Entitlements::grandfathered_until(), 'plan' => SixRsh_Entitlements::plan(), 'ever_paid' => SixRsh_Entitlements::ever_paid(), ), 'billingUrl' => 'https://www.6rsh.com/billing', 'cartRevivalUrl' => 'https://cartrevival.com', 'isRtl' => is_rtl(), 'eventVariables' => $event_variables_js, 'fieldOptions' => self::get_field_options(), 'defaultFields' => array( 'first_name', 'order_number', 'order_total', 'store_name', 'order_status', 'store_url' ), 'strings' => array( 'saving' => __( 'Saving...', '6rsh-messaging-notifications' ), 'saved' => __( 'Saved!', '6rsh-messaging-notifications' ), 'error' => __( 'Error. Try again.', '6rsh-messaging-notifications' ), 'connecting' => __( 'Connecting...', '6rsh-messaging-notifications' ), 'connected' => __( 'Connected!', '6rsh-messaging-notifications' ), 'testing' => __( 'Testing...', '6rsh-messaging-notifications' ), 'test_ok' => __( 'Connection OK', '6rsh-messaging-notifications' ), 'sending' => __( 'Sending...', '6rsh-messaging-notifications' ), 'sent' => __( 'Sent!', '6rsh-messaging-notifications' ), 'clearing' => __( 'Clearing...', '6rsh-messaging-notifications' ), 'cleared' => __( 'Cleared!', '6rsh-messaging-notifications' ), 'confirm_clear' => __( 'Are you sure you want to clear all logs?', '6rsh-messaging-notifications' ), 'no_test_phone' => __( 'No test phone number configured. Add one in the Notifications tab.', '6rsh-messaging-notifications' ), 'notif_locked' => __( 'Free plan includes 1 active notification. Disable the active one to switch, or upgrade for unlimited.', '6rsh-messaging-notifications' ), 'cart_locked' => __( 'Abandoned cart recovery is available on 6rsh paid plans.', '6rsh-messaging-notifications' ), 'rechecking' => __( 'Checking your plan…', '6rsh-messaging-notifications' ), 'recheck_paid' => __( 'Your plan is up to date — paid features unlocked.', '6rsh-messaging-notifications' ), 'recheck_free' => __( 'Your plan is up to date.', '6rsh-messaging-notifications' ), 'recheck_stale' => __( 'Couldn\'t reach 6rsh — using your last known plan.', '6rsh-messaging-notifications' ), ), ) ); } catch ( \Throwable $e ) { error_log( '[6rsh WhatsApp] Admin::enqueue_assets exception: ' . $e->getMessage() ); } } // ------------------------------------------------------------------------- // AJAX helpers // ------------------------------------------------------------------------- /** * Shared nonce + capability check for all admin AJAX handlers. * * @return void */ private static function verify_request(): void { check_ajax_referer( 'sixrsh_admin', 'nonce' ); if ( ! current_user_can( 'manage_options' ) ) { wp_send_json_error( array( 'message' => __( 'Permission denied.', '6rsh-messaging-notifications' ) ), 403 ); } } // ------------------------------------------------------------------------- // Wizard AJAX // ------------------------------------------------------------------------- /** * AJAX: validate API key and save connection data. * * @return void */ public static function ajax_wizard_connect(): void { try { self::verify_request(); $key = sanitize_text_field( wp_unslash( $_POST['api_key'] ?? '' ) ); if ( ! $key ) { wp_send_json_error( array( 'message' => __( 'API key is required.', '6rsh-messaging-notifications' ) ) ); } $api = new SixRsh_API( $key ); $result = $api->ping(); if ( ! $result['success'] ) { wp_send_json_error( array( 'message' => $result['error'] ?: __( 'Connection failed.', '6rsh-messaging-notifications' ), 'code' => $result['error_code'] ?? '', ) ); } $data = $result['data'] ?? array(); $account = is_array( $data['account'] ?? null ) ? $data['account'] : array(); // Key change → drop prior entitlement state (paid status / secret). $prev_key = (string) get_option( 'sixrsh_api_key', '' ); if ( $prev_key !== $key ) { SixRsh_Entitlements::reset(); } // Always save the validated key even when phone isn't connected yet. update_option( 'sixrsh_api_key', $key ); update_option( 'sixrsh_connected_account', array( 'phone_number' => $account['phone_number'] ?? '', 'name' => $account['name'] ?? '', 'status' => $account['status'] ?? '', 'plan' => $data['plan'] ?? '', ) ); update_option( 'sixrsh_connected_phone', $account['phone_number'] ?? '' ); // Register plan.changed callback + prime entitlements (best-effort). SixRsh_Entitlements::register_callback(); SixRsh_Entitlements::live(); wp_send_json_success( array( 'name' => $account['name'] ?? '', 'phone' => $account['phone_number'] ?? null, 'phone_connected' => ! empty( $account['phone_number'] ), ) ); } catch ( \Throwable $e ) { error_log( '[6rsh WhatsApp] ajax_wizard_connect exception: ' . $e->getMessage() ); wp_send_json_error( array( 'message' => __( 'An unexpected error occurred.', '6rsh-messaging-notifications' ) ) ); } } /** * AJAX: run a single wizard setup step. * * @return void */ public static function ajax_wizard_step(): void { try { self::verify_request(); $action_name = sanitize_key( wp_unslash( $_POST['action_name'] ?? '' ) ); switch ( $action_name ) { case 'fetch_templates': self::step_fetch_templates(); break; case 'match_templates': self::step_match_templates(); break; case 'detect_country': self::step_detect_country(); break; case 'setup_cart': self::step_setup_cart(); break; case 'setup_widget': self::step_setup_widget(); break; default: wp_send_json_error( array( 'message' => __( 'Unknown wizard step.', '6rsh-messaging-notifications' ) ) ); } } catch ( \Throwable $e ) { error_log( '[6rsh WhatsApp] ajax_wizard_step exception: ' . $e->getMessage() ); wp_send_json_error( array( 'message' => __( 'An unexpected error occurred.', '6rsh-messaging-notifications' ) ) ); } } /** * AJAX: mark wizard as complete. * * @return void */ public static function ajax_wizard_complete(): void { try { self::verify_request(); update_option( 'sixrsh_wizard_complete', 1 ); $settings = get_option( 'sixrsh_settings', array() ); $events = $settings['events'] ?? array(); $enabled = count( array_filter( $events, fn( $e ) => ! empty( $e['enabled'] ) ) ); wp_send_json_success( array( 'events_enabled' => $enabled, 'cart_enabled' => ! empty( $settings['abandoned_cart']['enabled'] ), 'widget_enabled' => ! empty( $settings['widget']['enabled'] ), 'settings_url' => admin_url( 'admin.php?page=6rsh-messaging-notifications' ), ) ); } catch ( \Throwable $e ) { error_log( '[6rsh WhatsApp] ajax_wizard_complete exception: ' . $e->getMessage() ); wp_send_json_error( array( 'message' => __( 'An unexpected error occurred.', '6rsh-messaging-notifications' ) ) ); } } // ------------------------------------------------------------------------- // Settings AJAX // ------------------------------------------------------------------------- /** * AJAX: save plugin settings. * * @return void */ public static function ajax_save_settings(): void { try { self::verify_request(); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- sanitized below $raw = $_POST['settings'] ?? array(); if ( ! is_array( $raw ) ) { wp_send_json_error( array( 'message' => __( 'Invalid settings data.', '6rsh-messaging-notifications' ) ) ); } $sanitized = self::sanitize_settings( $raw ); update_option( 'sixrsh_settings', $sanitized ); wp_send_json_success( array( 'message' => __( 'Settings saved.', '6rsh-messaging-notifications' ) ) ); } catch ( \Throwable $e ) { error_log( '[6rsh WhatsApp] ajax_save_settings exception: ' . $e->getMessage() ); wp_send_json_error( array( 'message' => __( 'An unexpected error occurred.', '6rsh-messaging-notifications' ) ) ); } } /** * AJAX: test API connection (ping). * * @return void */ public static function ajax_test_connection(): void { try { self::verify_request(); $api = SixRsh_API::instance(); if ( ! $api ) { wp_send_json_error( array( 'message' => __( 'No API key configured.', '6rsh-messaging-notifications' ) ) ); } $result = $api->ping(); if ( $result['success'] ) { $account = $result['data']['account'] ?? array(); // Refresh stored phone from the ping response. if ( ! empty( $account['phone_number'] ) ) { update_option( 'sixrsh_connected_phone', $account['phone_number'] ); update_option( 'sixrsh_connected_account', array( 'phone_number' => $account['phone_number'], 'name' => $account['name'] ?? '', 'status' => $account['status'] ?? '', 'plan' => $result['data']['plan'] ?? '', ) ); } delete_option( 'sixrsh_connection_error' ); wp_send_json_success( array( 'message' => __( 'Connection successful!', '6rsh-messaging-notifications' ), 'account' => $account, ) ); } else { update_option( 'sixrsh_connection_error', $result['error'] ?? 'Connection failed.' ); wp_send_json_error( array( 'message' => $result['error'] ?: __( 'Connection failed.', '6rsh-messaging-notifications' ) ) ); } } catch ( \Throwable $e ) { error_log( '[6rsh WhatsApp] ajax_test_connection exception: ' . $e->getMessage() ); wp_send_json_error( array( 'message' => __( 'An unexpected error occurred.', '6rsh-messaging-notifications' ) ) ); } } /** * AJAX: send a test notification. * * @return void */ public static function ajax_test_send(): void { try { self::verify_request(); $event_key = sanitize_key( wp_unslash( $_POST['event_key'] ?? 'order_placed' ) ); $settings = get_option( 'sixrsh_settings', array() ); $test_phone = sanitize_text_field( $settings['test_phone'] ?? '' ); if ( ! $test_phone ) { $account = get_option( 'sixrsh_connected_account', array() ); $test_phone = $account['phone_number'] ?? ''; } if ( ! $test_phone ) { wp_send_json_error( array( 'message' => __( 'No test phone number configured. Add one in the Notifications tab.', '6rsh-messaging-notifications' ) ) ); } $country_code = get_option( 'sixrsh_country_code', 'AE' ); $phone = SixRsh_Phone::normalize( $test_phone, (string) $country_code ); if ( ! $phone ) { wp_send_json_error( array( 'message' => __( 'Invalid test phone number.', '6rsh-messaging-notifications' ) ) ); } // Read template: per-option format first, fall back to legacy mapping. $tmpl_raw = get_option( 'sixrsh_event_' . $event_key . '_template', '' ); $event_map = $tmpl_raw ? json_decode( $tmpl_raw, true ) : null; if ( ! $event_map ) { $legacy = get_option( 'sixrsh_template_mapping', array() ); $event_map = $legacy[ $event_key ] ?? null; } $template_id = $event_map['template_id'] ?? null; $template_name = $event_map['template_name'] ?? ''; if ( ! $template_id ) { wp_send_json_error( array( 'message' => __( 'No template mapped for this event.', '6rsh-messaging-notifications' ) ) ); } // Load saved variable mapping; fall back to smart defaults trimmed to template var count. $var_map_raw = get_option( 'sixrsh_event_' . $event_key . '_variables', '' ); $var_mapping = $var_map_raw ? json_decode( $var_map_raw, true ) : array(); if ( empty( $var_mapping ) ) { $available_templates = get_option( 'sixrsh_available_templates', array() ); $var_count = 0; foreach ( $available_templates as $t ) { if ( ( $template_name && ( $t['name'] ?? '' ) === $template_name ) || ( $template_id && (string) ( $t['id'] ?? '' ) === (string) $template_id ) ) { $var_count = (int) ( $t['variables_count'] ?? 0 ); break; } } $default_fields = array( 'first_name', 'order_number', 'order_total', 'store_name', 'order_status', 'store_url' ); for ( $i = 1; $i <= $var_count; $i++ ) { $var_mapping[] = array( 'key' => (string) $i, 'field' => $default_fields[ $i - 1 ] ?? 'store_name' ); } } // Resolve with fake test data. $test_data = array( 'first_name' => __( 'Test Customer', '6rsh-messaging-notifications' ), 'last_name' => 'Test', 'full_name' => __( 'Test Customer', '6rsh-messaging-notifications' ), 'order_number' => '#TEST-001', 'order_total' => get_woocommerce_currency() . ' 100.00', 'order_status' => __( 'Processing', '6rsh-messaging-notifications' ), 'order_date' => current_time( get_option( 'date_format' ) ), 'store_name' => get_bloginfo( 'name' ), 'store_url' => get_site_url(), 'product_names' => __( 'Test Product', '6rsh-messaging-notifications' ), 'payment_method' => __( 'Credit Card', '6rsh-messaging-notifications' ), 'billing_phone' => $phone, 'billing_city' => 'Dubai', 'billing_country'=> 'AE', 'custom' => '', ); $variables = array(); foreach ( $var_mapping as $item ) { $field = $item['field'] ?? 'first_name'; $variables[ $item['key'] ] = $field === 'custom' ? ( $item['value'] ?? '' ) : ( $test_data[ $field ] ?? '' ); } $template_ref = $template_name ?: (string) $template_id; $api = SixRsh_API::instance(); $result = $api ? $api->send_notification( array( 'template' => $template_ref, 'recipients' => array( array( 'phone' => $phone, 'variables' => $variables, ) ), ) ) : array( 'success' => false, 'error' => __( 'No API key.', '6rsh-messaging-notifications' ) ); if ( $result['success'] ) { SixRsh_Logger::log( $event_key, $phone, $template_name, 'success' ); wp_send_json_success( array( 'message' => sprintf( /* translators: %s: phone number */ __( 'Test sent to %s!', '6rsh-messaging-notifications' ), $phone ), ) ); } else { $err_msg = is_array( $result['error'] ) ? wp_json_encode( $result['error'] ) : (string) ( $result['error'] ?: __( 'Send failed.', '6rsh-messaging-notifications' ) ); SixRsh_Logger::log( $event_key, $phone, $template_name, 'failed', $err_msg ); wp_send_json_error( array( 'message' => $err_msg ) ); } } catch ( \Throwable $e ) { error_log( '[6rsh WhatsApp] ajax_test_send exception: ' . $e->getMessage() ); wp_send_json_error( array( 'message' => __( 'An unexpected error occurred.', '6rsh-messaging-notifications' ) ) ); } } /** * AJAX: re-fetch templates from the API. * * @return void */ public static function ajax_refresh_templates(): void { try { self::verify_request(); delete_transient( 'sixrsh_available_templates' ); $api = SixRsh_API::instance(); if ( ! $api ) { wp_send_json_error( array( 'message' => __( 'No API key configured.', '6rsh-messaging-notifications' ) ) ); } $result = $api->get_templates(); if ( ! $result['success'] ) { wp_send_json_error( array( 'message' => $result['error'] ?: __( 'Failed to fetch templates.', '6rsh-messaging-notifications' ) ) ); } $templates = $result['data']['templates'] ?? array(); update_option( 'sixrsh_available_templates', $templates ); set_transient( 'sixrsh_available_templates', $templates, HOUR_IN_SECONDS ); wp_send_json_success( array( 'templates' => $templates, 'message' => sprintf( /* translators: %d: template count */ _n( '%d template loaded.', '%d templates loaded.', count( $templates ), '6rsh-messaging-notifications' ), count( $templates ) ), ) ); } catch ( \Throwable $e ) { error_log( '[6rsh WhatsApp] ajax_refresh_templates exception: ' . $e->getMessage() ); wp_send_json_error( array( 'message' => __( 'An unexpected error occurred.', '6rsh-messaging-notifications' ) ) ); } } // ------------------------------------------------------------------------- // Logs AJAX // ------------------------------------------------------------------------- /** * AJAX: return recent logs as JSON. * * @return void */ public static function ajax_get_logs(): void { try { self::verify_request(); $filters = array(); if ( ! empty( $_POST['event_type'] ) ) { $filters['event_type'] = sanitize_key( wp_unslash( $_POST['event_type'] ) ); } if ( ! empty( $_POST['status'] ) ) { $filters['status'] = sanitize_key( wp_unslash( $_POST['status'] ) ); } $logs = SixRsh_Logger::get_logs( $filters, 200 ); wp_send_json_success( array( 'logs' => $logs ) ); } catch ( \Throwable $e ) { error_log( '[6rsh WhatsApp] ajax_get_logs exception: ' . $e->getMessage() ); wp_send_json_error( array( 'message' => __( 'An unexpected error occurred.', '6rsh-messaging-notifications' ) ) ); } } /** * AJAX: clear all logs. * * @return void */ public static function ajax_clear_logs(): void { try { self::verify_request(); SixRsh_Logger::clear_logs(); wp_send_json_success( array( 'message' => __( 'Logs cleared.', '6rsh-messaging-notifications' ) ) ); } catch ( \Throwable $e ) { error_log( '[6rsh WhatsApp] ajax_clear_logs exception: ' . $e->getMessage() ); wp_send_json_error( array( 'message' => __( 'An unexpected error occurred.', '6rsh-messaging-notifications' ) ) ); } } /** * AJAX: export logs as a CSV file download. * * @return void */ public static function ajax_export_logs(): void { try { self::verify_request(); $logs = SixRsh_Logger::get_logs( array(), 1000 ); header( 'Content-Type: text/csv; charset=utf-8' ); header( 'Content-Disposition: attachment; filename="6rsh-messaging-notifications-logs-' . gmdate( 'Y-m-d' ) . '.csv"' ); header( 'Pragma: no-cache' ); header( 'Expires: 0' ); $output = fopen( 'php://output', 'w' ); if ( $output ) { fputcsv( $output, array( 'Log ID', 'Timestamp', 'Event', 'Phone', 'Template', 'Status', 'Error' ) ); foreach ( $logs as $row ) { fputcsv( $output, array( $row['log_id'], $row['created_at'], $row['event_type'], $row['customer_phone'], $row['template_name'], $row['status'], $row['error_message'], ) ); } fclose( $output ); } wp_die(); } catch ( \Throwable $e ) { error_log( '[6rsh WhatsApp] ajax_export_logs exception: ' . $e->getMessage() ); wp_die( esc_html__( 'Export failed.', '6rsh-messaging-notifications' ) ); } } /** * AJAX: fetch cart recovery stats. * * @return void */ public static function ajax_get_cart_stats(): void { try { self::verify_request(); $api = SixRsh_API::instance(); if ( ! $api ) { wp_send_json_error( array( 'message' => __( 'No API key configured.', '6rsh-messaging-notifications' ) ) ); } $result = $api->get_cart_stats(); if ( $result['success'] ) { wp_send_json_success( $result['data'] ); } else { wp_send_json_error( array( 'message' => $result['error'] ) ); } } catch ( \Throwable $e ) { error_log( '[6rsh WhatsApp] ajax_get_cart_stats exception: ' . $e->getMessage() ); wp_send_json_error( array( 'message' => __( 'An unexpected error occurred.', '6rsh-messaging-notifications' ) ) ); } } // ------------------------------------------------------------------------- // Wizard step implementations // ------------------------------------------------------------------------- /** * Wizard step: fetch templates from API. * * @return void */ private static function step_fetch_templates(): void { $api = SixRsh_API::instance(); if ( ! $api ) { wp_send_json_error( array( 'message' => __( 'No API key configured.', '6rsh-messaging-notifications' ) ) ); } $result = $api->get_templates(); if ( ! $result['success'] ) { wp_send_json_error( array( 'message' => $result['error'] ?: __( 'Failed to fetch templates.', '6rsh-messaging-notifications' ) ) ); } $templates = $result['data']['templates'] ?? array(); update_option( 'sixrsh_available_templates', $templates ); set_transient( 'sixrsh_available_templates', $templates, HOUR_IN_SECONDS ); wp_send_json_success( array( 'count' => count( $templates ) ) ); } /** * Wizard step: auto-match templates to event keys using keyword matching. * * @return void */ private static function step_match_templates(): void { $templates = get_option( 'sixrsh_available_templates', array() ); $mapping = array(); $keywords = array( 'order_placed' => array( 'order_placed', 'new_order', 'order_received', 'order_place' ), 'order_confirmed' => array( 'order_confirmed', 'order_processing', 'confirmed', 'processing' ), 'order_completed' => array( 'order_completed', 'completed', 'delivered', 'complete' ), 'order_cancelled' => array( 'order_cancelled', 'cancelled', 'canceled', 'cancel' ), 'order_refunded' => array( 'order_refunded', 'refunded', 'refund' ), 'order_on_hold' => array( 'order_on_hold', 'on_hold', 'hold' ), 'order_failed' => array( 'order_failed', 'failed', 'payment_failed', 'fail' ), 'new_customer' => array( 'new_customer', 'welcome', 'registration', 'new_user' ), 'abandoned_cart' => array( 'abandoned_cart', 'abandoned', 'cart_recovery', 'cart' ), ); foreach ( $keywords as $event_key => $kw_list ) { $matched = null; foreach ( $kw_list as $kw ) { foreach ( $templates as $t ) { if ( stripos( $t['name'] ?? '', $kw ) !== false ) { $matched = $t; break 2; } } } if ( $matched ) { $mapping[ $event_key ] = array( 'template_id' => $matched['id'], 'template_name' => $matched['name'], ); } } update_option( 'sixrsh_template_mapping', $mapping ); wp_send_json_success( array( 'mapped' => count( $mapping ) ) ); } /** * Wizard step: detect and store the WC store country code. * * @return void */ private static function step_detect_country(): void { $country = ''; if ( function_exists( 'WC' ) && WC()->countries ) { $country = (string) WC()->countries->get_base_country(); } if ( ! $country ) { $country = 'AE'; } update_option( 'sixrsh_country_code', $country ); wp_send_json_success( array( 'country' => $country ) ); } /** * Wizard step: verify the carts table exists. * * @return void */ private static function step_setup_cart(): void { global $wpdb; try { $table = $wpdb->prefix . 'sixrsh_carts'; // phpcs:disable WordPress.DB.DirectDatabaseQuery, WordPress.DB.PreparedSQL.InterpolatedNotPrepared $result = $wpdb->get_var( "SELECT 1 FROM {$wpdb->prefix}sixrsh_carts LIMIT 1" ); // phpcs:enable wp_send_json_success( array( 'table' => $table ) ); } catch ( \Throwable $e ) { // Table missing — try to create it. SixRsh_Activator::activate(); wp_send_json_success( array( 'table' => 'created' ) ); } } /** * Wizard step: ensure widget defaults are populated in settings. * * @return void */ private static function step_setup_widget(): void { $defaults = self::default_settings(); $current = get_option( 'sixrsh_settings', array() ); if ( empty( $current ) ) { update_option( 'sixrsh_settings', $defaults ); wp_send_json_success( array( 'message' => 'Defaults applied.' ) ); } else { // Merge widget defaults only. if ( empty( $current['widget'] ) ) { $current['widget'] = $defaults['widget']; update_option( 'sixrsh_settings', $current ); } wp_send_json_success( array( 'message' => 'Settings OK.' ) ); } } // ------------------------------------------------------------------------- // Sanitization helpers // ------------------------------------------------------------------------- /** * Sanitize raw settings input from the UI. * * @param array $raw Unsanitized settings array from POST. * * @return array Sanitized settings. */ private static function sanitize_settings( array $raw ): array { $defaults = self::default_settings(); $out = $defaults; // Events. $event_keys = array_keys( $defaults['events'] ); if ( isset( $raw['events'] ) && is_array( $raw['events'] ) ) { foreach ( $event_keys as $key ) { if ( isset( $raw['events'][ $key ] ) ) { $out['events'][ $key ]['enabled'] = ! empty( $raw['events'][ $key ]['enabled'] ); } } } // Abandoned cart. if ( isset( $raw['abandoned_cart'] ) && is_array( $raw['abandoned_cart'] ) ) { $ac = $raw['abandoned_cart']; $out['abandoned_cart']['enabled'] = ! empty( $ac['enabled'] ); $out['abandoned_cart']['capture_guests'] = ! empty( $ac['capture_guests'] ); $out['abandoned_cart']['require_opt_in'] = ! empty( $ac['require_opt_in'] ); $out['abandoned_cart']['opt_in_default_checked'] = ! empty( $ac['opt_in_default_checked'] ); $out['abandoned_cart']['opt_in_label'] = isset( $ac['opt_in_label'] ) ? sanitize_text_field( $ac['opt_in_label'] ) : $defaults['abandoned_cart']['opt_in_label']; } // Consent. $out['consent_required'] = ! empty( $raw['consent_required'] ); // Test phone. $out['test_phone'] = isset( $raw['test_phone'] ) ? sanitize_text_field( $raw['test_phone'] ) : ''; // Widget. if ( isset( $raw['widget'] ) && is_array( $raw['widget'] ) ) { $w = $raw['widget']; $out['widget']['enabled'] = ! empty( $w['enabled'] ); $out['widget']['position'] = in_array( $w['position'] ?? '', array( 'left', 'right' ), true ) ? $w['position'] : 'right'; $out['widget']['greeting'] = isset( $w['greeting'] ) ? sanitize_text_field( $w['greeting'] ) : $defaults['widget']['greeting']; $out['widget']['prefill'] = isset( $w['prefill'] ) ? sanitize_text_field( $w['prefill'] ) : $defaults['widget']['prefill']; $out['widget']['show_on'] = in_array( $w['show_on'] ?? '', array( 'all', 'home', 'shop-product' ), true ) ? $w['show_on'] : 'all'; $out['widget']['color'] = isset( $w['color'] ) && preg_match( '/^#[0-9a-fA-F]{3,6}$/', $w['color'] ) ? $w['color'] : '#25D366'; $out['widget']['delay'] = isset( $w['delay'] ) ? max( 0, min( 60, (int) $w['delay'] ) ) : 3; $out['widget']['hide_mobile'] = ! empty( $w['hide_mobile'] ); } // Web Chat (v1.8.0). Whitelisted here deliberately: sanitize_settings // rebuilds the option from defaults, so a key that is NOT listed is // silently DROPPED on every save. That is a quiet way to lose a merchant's // configuration, so any new setting must be added here too. if ( isset( $raw['web_chat'] ) && is_array( $raw['web_chat'] ) ) { $wc = $raw['web_chat']; $out['web_chat']['enabled'] = ! empty( $wc['enabled'] ); // 32 lowercase alphanumerics, exactly as 6rsh mints them. Anything else // is a paste error and is rejected rather than stored. $key = isset( $wc['public_key'] ) ? strtolower( trim( (string) $wc['public_key'] ) ) : ''; $out['web_chat']['public_key'] = preg_match( '/^[a-z0-9]{32}$/', $key ) ? $key : ''; // Privacy controls default ON, but only because enabling Web Chat is // itself an explicit act by the merchant. $out['web_chat']['pass_identity'] = ! isset( $wc['pass_identity'] ) || ! empty( $wc['pass_identity'] ); $out['web_chat']['pass_cart'] = ! isset( $wc['pass_cart'] ) || ! empty( $wc['pass_cart'] ); } // Preserve template mapping in settings if passed. if ( isset( $raw['template_mapping'] ) && is_array( $raw['template_mapping'] ) ) { $clean_mapping = array(); foreach ( $raw['template_mapping'] as $event_key => $map ) { $event_key = sanitize_key( $event_key ); if ( $event_key && is_array( $map ) ) { $clean_mapping[ $event_key ] = array( 'template_id' => sanitize_text_field( $map['template_id'] ?? '' ), 'template_name' => sanitize_text_field( $map['template_name'] ?? '' ), ); } } update_option( 'sixrsh_template_mapping', $clean_mapping ); } return $out; } /** * AJAX: provision this store's Web Chat widget from 6rsh. * * Fetches (creating on first call) the account's widget key over the API key * this store already holds, and registers this domain on the server's Origin * allowlist. Both halves matter: the allowlist fails closed, so a key stored * without its domain produces a widget that loads and then silently refuses * to talk — the single hardest failure for a merchant to diagnose. * * @return void */ public static function ajax_webchat_provision(): void { try { check_ajax_referer( 'sixrsh_admin', 'nonce' ); if ( ! current_user_can( 'manage_options' ) ) { wp_send_json_error( array( 'message' => 'Permission denied.' ), 403 ); } $api = SixRsh_API::instance(); if ( ! $api ) { wp_send_json_error( array( 'message' => __( 'Connect your store to 6rsh first, on the Connection tab.', '6rsh-messaging-notifications' ), ), 400 ); } $domain = (string) wp_parse_url( home_url(), PHP_URL_HOST ); $res = $api->provision_web_chat( $domain ); if ( empty( $res['success'] ) || empty( $res['data']['widget']['public_key'] ) ) { $msg = ! empty( $res['error'] ) ? (string) $res['error'] : __( 'Could not set up Web Chat. Please try again.', '6rsh-messaging-notifications' ); wp_send_json_error( array( 'message' => $msg ), 502 ); } $widget = $res['data']['widget']; $key = strtolower( (string) $widget['public_key'] ); // Never store a key that is not the shape the widget loader expects; // a malformed one would fail at render time with nothing to show why. if ( ! preg_match( '/^[a-z0-9]{32}$/', $key ) ) { wp_send_json_error( array( 'message' => __( '6rsh returned an unexpected key. Please contact support.', '6rsh-messaging-notifications' ), ), 502 ); } $settings = get_option( 'sixrsh_settings', array() ); $settings = array_replace_recursive( self::default_settings(), $settings ); $settings['web_chat']['public_key'] = $key; update_option( 'sixrsh_settings', $settings ); $domains = isset( $widget['allowed_domains'] ) && is_array( $widget['allowed_domains'] ) ? $widget['allowed_domains'] : array(); wp_send_json_success( array( 'public_key' => $key, 'masked' => substr( $key, 0, 6 ) . '…' . substr( $key, -4 ), 'domain' => $domain, 'allowed_domains' => $domains, 'domain_ok' => in_array( strtolower( $domain ), array_map( 'strtolower', $domains ), true ), 'message' => __( 'Web Chat is set up and this domain is authorised.', '6rsh-messaging-notifications' ), ) ); } catch ( \Throwable $e ) { error_log( '[6rsh] ajax_webchat_provision exception: ' . $e->getMessage() ); wp_send_json_error( array( 'message' => 'An unexpected error occurred.' ), 500 ); } } /** * AJAX: auto-save individual setting. * * @return void */ public static function ajax_save_setting(): void { try { check_ajax_referer( 'sixrsh_admin', 'nonce' ); if ( ! current_user_can( 'manage_options' ) ) { wp_send_json_error( array( 'message' => 'Permission denied.' ), 403 ); } $path = sanitize_text_field( wp_unslash( $_POST['path'] ?? '' ) ); $value = sanitize_text_field( wp_unslash( $_POST['value'] ?? '' ) ); $name = sanitize_text_field( wp_unslash( $_POST['name'] ?? '' ) ); $settings = get_option( 'sixrsh_settings', array() ); $defaults = self::default_settings(); $settings = array_replace_recursive( $defaults, $settings ); $parts = explode( '.', $path ); if ( $parts[0] === 'template' && ! empty( $parts[1] ) ) { $event_key = sanitize_key( $parts[1] ); // Per-option format (v1.1.1+). if ( $value ) { update_option( "sixrsh_event_{$event_key}_template", wp_json_encode( array( 'template_id' => $value, 'template_name' => $name ) ) ); } else { delete_option( "sixrsh_event_{$event_key}_template" ); } // Legacy mapping (kept for back-compat). $mapping = get_option( 'sixrsh_template_mapping', array() ); if ( $value ) { $mapping[ $event_key ] = array( 'template_id' => $value, 'template_name' => $name ); } else { unset( $mapping[ $event_key ] ); } update_option( 'sixrsh_template_mapping', $mapping ); wp_send_json_success( array( 'message' => 'Saved.' ) ); } // Per-option event enabled (v1.1.1 format: event.wc-processing.enabled). if ( $parts[0] === 'event' && ! empty( $parts[1] ) && ( $parts[2] ?? '' ) === 'enabled' ) { $slug = sanitize_key( $parts[1] ); $enabling = ( $value === '1' ); // Notification limit gate. Toggling OFF is always allowed. Enabling // triggers a LIVE entitlement check first (spec B1a/B2) so a // just-upgraded user is unlocked instantly. Already-enabled events // re-saving themselves are not a new activation → never blocked. $already = (bool) get_option( 'sixrsh_event_' . $slug . '_enabled', false ); if ( $enabling && ! $already ) { $live = SixRsh_Entitlements::live(); // NEVER use `?? 1` here: null is the wire format for // UNLIMITED, and `??` fires on null — that capped Pro // tenants at 1 notification (v1.6.0 bug). $limit = SixRsh_Entitlements::limit_from( $live['entitlements'] ); if ( null !== $limit && SixRsh_Entitlements::enabled_notification_count() >= $limit ) { wp_send_json_error( array( 'code' => 'notifications_limit_reached', 'limit' => (int) $limit, 'message' => __( 'Free plan includes 1 active notification. Disable the active one to switch, or upgrade for unlimited.', '6rsh-messaging-notifications' ), ), 403 ); } } update_option( 'sixrsh_event_' . $slug . '_enabled', $enabling ); wp_send_json_success( array( 'message' => 'Saved.' ) ); } if ( $parts[0] === 'events' && ! empty( $parts[1] ) && ! empty( $parts[2] ) ) { $settings['events'][ sanitize_key( $parts[1] ) ]['enabled'] = ( $value === '1' ); } elseif ( $parts[0] === 'abandoned_cart' && ! empty( $parts[1] ) ) { $ac_key = sanitize_key( $parts[1] ); // Abandoned-cart gate: enabling sync triggers a LIVE check first. // Turning it off / editing other AC fields is always allowed. if ( 'enabled' === $ac_key && $value === '1' ) { $live = SixRsh_Entitlements::live(); if ( empty( $live['entitlements']['abandoned_carts'] ) ) { wp_send_json_error( array( 'code' => 'abandoned_carts_requires_paid_plan', 'message' => __( 'Abandoned cart recovery is available on 6rsh paid plans.', '6rsh-messaging-notifications' ), ), 403 ); } } // opt_in_label is free text; every other abandoned_cart key is boolean. $settings['abandoned_cart'][ $ac_key ] = ( 'opt_in_label' === $ac_key ) ? sanitize_text_field( $value ) : ( $value === '1' ); } elseif ( $parts[0] === 'consent_required' ) { $settings['consent_required'] = ( $value === '1' ); } elseif ( $parts[0] === 'test_phone' ) { $settings['test_phone'] = sanitize_text_field( $value ); } elseif ( $parts[0] === 'widget' && ! empty( $parts[1] ) ) { $wk = sanitize_key( $parts[1] ); switch ( $wk ) { case 'enabled': case 'hide_mobile': $settings['widget'][ $wk ] = ( $value === '1' ); break; case 'position': $settings['widget']['position'] = in_array( $value, array( 'left', 'right' ), true ) ? $value : 'right'; break; case 'show_on': $settings['widget']['show_on'] = in_array( $value, array( 'all', 'home', 'shop-product' ), true ) ? $value : 'all'; break; case 'greeting': case 'prefill': $settings['widget'][ $wk ] = sanitize_text_field( $value ); break; case 'color': $settings['widget']['color'] = preg_match( '/^#[0-9a-fA-F]{3,6}$/', $value ) ? $value : '#25D366'; break; case 'delay': $settings['widget']['delay'] = max( 0, min( 60, (int) $value ) ); break; default: wp_send_json_error( array( 'message' => 'Unknown setting: ' . $path ), 400 ); } } elseif ( $parts[0] === 'web_chat' && ! empty( $parts[1] ) ) { $ck = sanitize_key( $parts[1] ); switch ( $ck ) { case 'enabled': case 'pass_identity': case 'pass_cart': $settings['web_chat'][ $ck ] = ( $value === '1' ); break; case 'public_key': // Empty clears the key (turning Web Chat off in effect); // anything else must be the exact 32-char lowercase key // shape the server mints, so a mistyped value is rejected // here rather than becoming a widget that renders nothing. $key = strtolower( trim( $value ) ); if ( '' !== $key && ! preg_match( '/^[a-z0-9]{32}$/', $key ) ) { wp_send_json_error( array( 'message' => __( 'That widget key does not look right. It is 32 letters and numbers.', '6rsh-messaging-notifications' ), ), 400 ); } $settings['web_chat']['public_key'] = $key; break; default: wp_send_json_error( array( 'message' => 'Unknown setting: ' . $path ), 400 ); } } elseif ( $parts[0] === 'widget_mode' ) { // ONE control for "what appears in the corner of my store". // // The two surfaces were previously two independent booleans on two // separate tabs, so "both on" was reachable and produced two // overlapping buttons — a state the UI could only warn about after // the fact. Making the choice exclusive at the point of writing // means the bad state cannot be stored at all. $mode = in_array( $value, array( 'web_chat', 'whatsapp', 'off' ), true ) ? $value : 'off'; $settings['web_chat']['enabled'] = ( 'web_chat' === $mode ); $settings['widget']['enabled'] = ( 'whatsapp' === $mode ); } else { // Anything unrecognised is a bug in the page, not a no-op. Falling // through to a cheerful "Saved." is how a whole settings tab can // look like it works while persisting nothing. wp_send_json_error( array( 'message' => 'Unknown setting: ' . $path ), 400 ); } update_option( 'sixrsh_settings', $settings ); wp_send_json_success( array( 'message' => 'Saved.' ) ); } catch ( \Throwable $e ) { error_log( '[6rsh WhatsApp] ajax_save_setting exception: ' . $e->getMessage() ); wp_send_json_error( array( 'message' => 'An unexpected error occurred.' ) ); } } /** * AJAX: batch-save all notification settings from the Notifications tab. * * @return void */ public static function ajax_save_notifications(): void { try { check_ajax_referer( 'sixrsh_admin', 'nonce' ); if ( ! current_user_can( 'manage_options' ) ) { wp_send_json_error( array( 'message' => 'Permission denied.' ), 403 ); } // Save test phone and consent_required into sixrsh_settings. $settings = get_option( 'sixrsh_settings', array() ); if ( isset( $_POST['test_phone'] ) ) { $settings['test_phone'] = sanitize_text_field( wp_unslash( $_POST['test_phone'] ) ); } $settings['consent_required'] = ! empty( $_POST['consent_required'] ); update_option( 'sixrsh_settings', $settings ); // Save each event enabled state to per-option format. When the paid // notification limit is in force, enforce it here too: keep everything // already enabled (grace for pre-existing multiples), honour every // disable, but never let this batch push the ENABLED count above the // limit. A live check first so a just-upgraded user is uncapped. // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized $events = isset( $_POST['event'] ) && is_array( $_POST['event'] ) ? $_POST['event'] : array(); $live = SixRsh_Entitlements::live(); // Unlimited-safe read (see limit_from(); `?? 1` would cap paid plans). $limit = SixRsh_Entitlements::limit_from( $live['entitlements'] ); // Currently-enabled set (before this save) — these are grandfathered. $already_on = array(); foreach ( SixRsh_Entitlements::all_event_slugs() as $s ) { if ( get_option( 'sixrsh_event_' . $s . '_enabled', false ) ) { $already_on[ $s ] = true; } } $on_count = count( $already_on ); foreach ( $events as $raw_key => $enabled ) { $key = sanitize_key( $raw_key ); if ( ! $key ) { continue; } $want_on = ! empty( $enabled ); if ( null !== $limit && $want_on && empty( $already_on[ $key ] ) ) { // New activation — only allow if under the limit. if ( $on_count >= (int) $limit ) { continue; // silently skip; JS shows the lock note } $on_count++; } elseif ( ! $want_on && ! empty( $already_on[ $key ] ) ) { $on_count--; } update_option( "sixrsh_event_{$key}_enabled", $want_on ); } wp_send_json_success( array( 'message' => __( 'Settings saved.', '6rsh-messaging-notifications' ) ) ); } catch ( \Throwable $e ) { error_log( '[6rsh WhatsApp] ajax_save_notifications exception: ' . $e->getMessage() ); wp_send_json_error( array( 'message' => 'An unexpected error occurred.' ) ); } } /** * One-time migration: copy event enabled states and template mappings from * the legacy sixrsh_settings['events'] + sixrsh_template_mapping format to * individual sixrsh_event_{slug}_enabled / sixrsh_event_{slug}_template options. * * Run on every admin init; guarded by sixrsh_events_migrated_v2 flag. * * @return void */ public static function maybe_migrate_events_v2(): void { if ( get_option( 'sixrsh_events_migrated_v2' ) ) { return; } $key_map = array( 'order_placed' => 'order_placed', 'order_confirmed' => 'wc-processing', 'order_completed' => 'wc-completed', 'order_cancelled' => 'wc-cancelled', 'order_refunded' => 'wc-refunded', 'order_on_hold' => 'wc-on-hold', 'order_failed' => 'wc-failed', 'new_customer' => 'new_customer', ); $old_settings = get_option( 'sixrsh_settings', array() ); $old_events = $old_settings['events'] ?? array(); $old_mapping = get_option( 'sixrsh_template_mapping', array() ); foreach ( $key_map as $old_key => $new_slug ) { if ( isset( $old_events[ $old_key ] ) ) { update_option( "sixrsh_event_{$new_slug}_enabled", ! empty( $old_events[ $old_key ]['enabled'] ) ); } if ( ! empty( $old_mapping[ $old_key ] ) ) { update_option( "sixrsh_event_{$new_slug}_template", wp_json_encode( $old_mapping[ $old_key ] ) ); } } update_option( 'sixrsh_events_migrated_v2', 1 ); } /** * AJAX: dismiss setup banner. * * @return void */ public static function ajax_dismiss_banner(): void { check_ajax_referer( 'sixrsh_admin', 'nonce' ); if ( current_user_can( 'manage_options' ) ) { delete_option( 'sixrsh_setup_banner_show' ); } wp_send_json_success(); } /** * AJAX: force a LIVE plan re-check (the escape-hatch button, spec B1f). * Returns the fresh entitlements so the page can repaint without reload. * * @return void */ public static function ajax_recheck_plan(): void { try { check_ajax_referer( 'sixrsh_admin', 'nonce' ); if ( ! current_user_can( 'manage_options' ) ) { wp_send_json_error( array( 'message' => 'Permission denied.' ), 403 ); } // HARD clear first: drop BOTH the 5-min transient and the persisted // last-known snapshot, so this really re-derives from the server // instead of echoing whatever stale value was already stored. // (v1.6.0 only dropped the transient, so a bad snapshot survived and // the button appeared to do nothing.) OPTION_EVER_PAID is kept so a // proven-paid store can never be re-locked by pressing re-check. SixRsh_Entitlements::hard_clear(); $live = SixRsh_Entitlements::live(); $e = $live['entitlements']; wp_send_json_success( array( 'ok' => $live['ok'], 'stale' => $live['stale'], 'plan' => $e['plan'] ?? 'free', 'abandoned_carts' => ! empty( $e['abandoned_carts'] ), 'notifications_limit' => SixRsh_Entitlements::limit_from( $e ), 'enabled_count' => SixRsh_Entitlements::enabled_notification_count(), 'grandfathered_until' => $e['grandfathered_until'] ?? null, ) ); } catch ( \Throwable $ex ) { error_log( '[6rsh WhatsApp] ajax_recheck_plan exception: ' . $ex->getMessage() ); wp_send_json_error( array( 'message' => 'An unexpected error occurred.' ) ); } } /** * AJAX: bust the passive entitlement cache — fired when the user clicks an * Upgrade CTA so their return visit fetches fresh (spec B1d). * * @return void */ public static function ajax_bust_entitlements(): void { check_ajax_referer( 'sixrsh_admin', 'nonce' ); if ( current_user_can( 'manage_options' ) ) { SixRsh_Entitlements::bust(); } wp_send_json_success(); } /** * AJAX: dismiss a CartRevival recommendation card (per-user user-meta). * * @return void */ public static function ajax_dismiss_cr_card(): void { check_ajax_referer( 'sixrsh_admin', 'nonce' ); if ( current_user_can( 'manage_options' ) ) { $which = sanitize_key( wp_unslash( $_POST['card'] ?? 'locked' ) ); update_user_meta( get_current_user_id(), 'sixrsh_cr_card_dismissed_' . $which, 1 ); } wp_send_json_success(); } /** * AJAX: merchant override for cart-engine ownership. * * '1' => keep 6rsh's own cart engine even though CartRevival is installed. * '0' => resume the default (yield to CartRevival). * * @return void */ public static function ajax_set_cart_engine_override(): void { check_ajax_referer( 'sixrsh_admin', 'nonce' ); if ( ! current_user_can( 'manage_options' ) ) { wp_send_json_error( array( 'message' => 'Permission denied.' ), 403 ); } $on = '1' === (string) ( $_POST['enabled'] ?? '' ); if ( $on ) { update_option( SixRsh_Coexistence::OPTION_OVERRIDE, '1', false ); // Merchant wants 6rsh to own carts — record that in the shared key // so CartRevival can see it and stand down. update_option( SixRsh_Coexistence::OPTION_ENGINE_OWNER, SixRsh_Coexistence::OWNER_6RSH, false ); } else { delete_option( SixRsh_Coexistence::OPTION_OVERRIDE ); // Hand ownership back to CartRevival when it is present. if ( SixRsh_Coexistence::cartrevival_active() ) { update_option( SixRsh_Coexistence::OPTION_ENGINE_OWNER, SixRsh_Coexistence::OWNER_CARTREVIVAL, false ); } } wp_send_json_success( array( 'override' => $on ) ); } /** * AJAX: dismiss onboarding checklist. * * @return void */ public static function ajax_dismiss_checklist(): void { check_ajax_referer( 'sixrsh_admin', 'nonce' ); if ( current_user_can( 'manage_options' ) ) { update_option( 'sixrsh_checklist_dismissed', 1 ); } wp_send_json_success(); } /** * Count failed log entries in the last 24 hours (cached 5 min). * * @return int */ private static function count_recent_failures(): int { $cached = get_transient( 'sixrsh_failure_count' ); if ( false !== $cached ) return (int) $cached; global $wpdb; $table = $wpdb->prefix . 'sixrsh_logs'; if ( $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $table ) ) !== $table ) { return 0; } // phpcs:disable WordPress.DB.DirectDatabaseQuery,WordPress.DB.PreparedSQL.InterpolatedNotPrepared $count = (int) $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM {$wpdb->prefix}sixrsh_logs WHERE status = %s AND created_at >= %s", 'failed', gmdate( 'Y-m-d H:i:s', time() - DAY_IN_SECONDS ) ) ); // phpcs:enable set_transient( 'sixrsh_failure_count', $count, 5 * MINUTE_IN_SECONDS ); return $count; } /** * Return the human-readable field options for the variable mapping UI. * * @return array */ public static function get_field_options(): array { return array( 'first_name' => __( 'Customer First Name', '6rsh-messaging-notifications' ), 'last_name' => __( 'Customer Last Name', '6rsh-messaging-notifications' ), 'full_name' => __( 'Customer Full Name', '6rsh-messaging-notifications' ), 'order_number' => __( 'Order Number (#1234)', '6rsh-messaging-notifications' ), 'order_total' => __( 'Order Total', '6rsh-messaging-notifications' ), 'order_status' => __( 'Order Status', '6rsh-messaging-notifications' ), 'order_date' => __( 'Order Date', '6rsh-messaging-notifications' ), 'store_name' => __( 'Store Name', '6rsh-messaging-notifications' ), 'store_url' => __( 'Store URL', '6rsh-messaging-notifications' ), 'product_names' => __( 'Product Names', '6rsh-messaging-notifications' ), 'payment_method' => __( 'Payment Method', '6rsh-messaging-notifications' ), 'billing_phone' => __( 'Billing Phone', '6rsh-messaging-notifications' ), 'billing_city' => __( 'Billing City', '6rsh-messaging-notifications' ), 'billing_country'=> __( 'Billing Country', '6rsh-messaging-notifications' ), 'custom' => __( 'Custom text...', '6rsh-messaging-notifications' ), ); } /** * AJAX: save a single event row (template + variable mapping). * * @return void */ public static function ajax_save_event_row(): void { try { self::verify_request(); $event_key = sanitize_key( wp_unslash( $_POST['event_key'] ?? '' ) ); if ( ! $event_key ) { wp_send_json_error( array( 'message' => __( 'Missing event key.', '6rsh-messaging-notifications' ) ) ); } // Save template. $template_id = sanitize_text_field( wp_unslash( $_POST['template_id'] ?? '' ) ); $template_name = sanitize_text_field( wp_unslash( $_POST['template_name'] ?? '' ) ); if ( $template_id ) { update_option( "sixrsh_event_{$event_key}_template", wp_json_encode( array( 'template_id' => $template_id, 'template_name' => $template_name ) ) ); // Keep legacy mapping in sync. $mapping = get_option( 'sixrsh_template_mapping', array() ); $mapping[ $event_key ] = array( 'template_id' => $template_id, 'template_name' => $template_name ); update_option( 'sixrsh_template_mapping', $mapping ); } // Save variable mapping. // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized $raw_vars = isset( $_POST['variables'] ) && is_array( $_POST['variables'] ) ? $_POST['variables'] : array(); $clean_vars = array(); foreach ( $raw_vars as $item ) { if ( ! is_array( $item ) ) continue; $key = sanitize_text_field( $item['key'] ?? '' ); $field = sanitize_key( $item['field'] ?? '' ); $value = sanitize_text_field( $item['value'] ?? '' ); if ( $key && $field ) { $clean_vars[] = array( 'key' => $key, 'field' => $field, 'value' => $value ); } } if ( ! empty( $clean_vars ) ) { update_option( "sixrsh_event_{$event_key}_variables", wp_json_encode( $clean_vars ) ); } else { delete_option( "sixrsh_event_{$event_key}_variables" ); } wp_send_json_success( array( 'message' => __( 'Saved.', '6rsh-messaging-notifications' ) ) ); } catch ( \Throwable $e ) { error_log( '[6rsh WhatsApp] ajax_save_event_row exception: ' . $e->getMessage() ); wp_send_json_error( array( 'message' => __( 'An unexpected error occurred.', '6rsh-messaging-notifications' ) ) ); } } /** * Return the default settings array. * * @return array */ public static function default_settings(): array { return array( // Website live chat. Ships OFF: a merchant must paste their public // key before anything is injected into their storefront. 'web_chat' => array( 'enabled' => false, 'public_key' => '', 'pass_identity' => true, 'pass_cart' => true, ), 'events' => array( 'order_placed' => array( 'enabled' => true ), 'order_confirmed' => array( 'enabled' => true ), 'order_completed' => array( 'enabled' => true ), 'order_cancelled' => array( 'enabled' => false ), 'order_refunded' => array( 'enabled' => false ), 'order_on_hold' => array( 'enabled' => false ), 'order_failed' => array( 'enabled' => false ), 'new_customer' => array( 'enabled' => false ), ), 'abandoned_cart' => array( 'enabled' => true, 'capture_guests' => true, 'require_opt_in' => true, 'opt_in_default_checked' => true, 'opt_in_label' => 'Send me order updates and offers on WhatsApp', ), 'consent_required' => false, 'test_phone' => '', 'widget' => array( 'enabled' => true, 'position' => 'right', 'greeting' => 'Chat with us on WhatsApp 👋', 'prefill' => 'Hello! I have a question about...', 'show_on' => 'all', 'color' => '#25D366', 'delay' => 3, 'hide_mobile' => false, ), ); } }