'
. ''
. ''
);
$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,
),
);
}
}