'; esc_html_e( '6rsh WhatsApp requires WooCommerce to be installed and activated.', '6rsh-messaging-notifications' ); echo '

'; } ); return; } SixRsh_Activator::maybe_upgrade(); SixRsh_Activator::migrate_legacy_settings(); SixRsh_Activator::check_redirect(); SixRsh_Cron::maybe_upgrade_cleanup(); SixRsh_Cron::init(); SixRsh_Order_Hooks::init(); SixRsh_Followups::init(); SixRsh_Cart_Tracker::init(); SixRsh_Checkout::init(); SixRsh_Widget::init(); if ( is_admin() ) { SixRsh_Admin::init(); } } ); // ── Wizard REST API (bypasses admin-ajax.php — works even with WAFs) ────────── add_action( 'rest_api_init', function () { if ( ! class_exists( 'SixRsh_Admin' ) ) { require_once SIXRSH_PATH . 'admin/class-admin.php'; } $auth = array( 'permission_callback' => function () { return current_user_can( 'manage_options' ); } ); register_rest_route( '6rsh/v1', '/connect', array_merge( array( 'methods' => 'POST', 'callback' => 'sixrsh_rest_connect', ), $auth ) ); register_rest_route( '6rsh/v1', '/setup', array_merge( array( 'methods' => 'POST', 'callback' => 'sixrsh_rest_setup', ), $auth ) ); register_rest_route( '6rsh/v1', '/complete', array_merge( array( 'methods' => 'POST', 'callback' => 'sixrsh_rest_complete', ), $auth ) ); // Follow-up settings save (moved off admin-post.php — Cloudways WAF blocks it). register_rest_route( '6rsh/v1', '/save-followups', array( 'methods' => 'POST', 'callback' => 'sixrsh_rest_save_followups', 'permission_callback' => function () { return current_user_can( 'manage_options' ); }, ) ); // Live abandoned-cart capture from the checkout page (guests + logged-in). // PUBLIC by necessity (guests aren't authenticated), but protected by the // WordPress REST nonce verified inside the callback. Uses REST, NOT // admin-ajax.php, which Cloudways' WAF blocks for unauthenticated posts. register_rest_route( '6rsh/v1', '/capture-cart', array( 'methods' => 'POST', 'callback' => 'sixrsh_rest_capture_cart', 'permission_callback' => '__return_true', ) ); // Signed plan.changed webhook receiver (HMAC-verified inside the handler). SixRsh_Entitlements::register_webhook_route(); } ); /** * REST: capture a phone/email + WhatsApp opt-in live during checkout. * * Stores the pending abandoned cart (server-side, in the plugin's WP tables and * the 6rsh platform) so recovery works for guests too — before the order is ever * placed. The phone is normalized to E.164 and discarded if invalid; nothing is * stored unless the opt-in requirement is satisfied. */ function sixrsh_rest_capture_cart( WP_REST_Request $request ): WP_REST_Response { // Nonce check — the checkout script sends X-WP-Nonce (wp_create_nonce('wp_rest')). $nonce = $request->get_header( 'X-WP-Nonce' ); if ( ! $nonce || ! wp_verify_nonce( $nonce, 'wp_rest' ) ) { return new WP_REST_Response( array( 'success' => false, 'reason' => 'bad_nonce' ), 403 ); } $phone = (string) ( $request->get_param( 'phone' ) ?? '' ); $email = sanitize_email( (string) ( $request->get_param( 'email' ) ?? '' ) ); $opt_in = filter_var( $request->get_param( 'opt_in' ), FILTER_VALIDATE_BOOLEAN ); $result = SixRsh_Cart_Tracker::capture_live( $phone, $email, $opt_in ); return new WP_REST_Response( $result, $result['success'] ? 200 : 200 ); } /** * REST: save delayed follow-up rules + order sync toggle. * * Replaces the legacy admin-post.php handler, which Cloudways' WAF blocks * (the request was rejected with "Sorry, you are not allowed to access this * page"). Auth is the X-WP-Nonce header + manage_options capability. */ function sixrsh_rest_save_followups( WP_REST_Request $request ): WP_REST_Response { $rules = $request->get_param( 'rules' ); if ( ! is_array( $rules ) ) { $rules = array(); } for ( $i = 1; $i <= 2; $i++ ) { $raw = $rules[ $i - 1 ] ?? array(); if ( ! is_array( $raw ) ) { $raw = array(); } $tmpl = sanitize_text_field( $raw['template'] ?? '' ); list( $tid, $tname ) = array_pad( explode( '|', $tmpl, 2 ), 2, '' ); update_option( 'sixrsh_followup_rule_' . $i, array( 'enabled' => ! empty( $raw['enabled'] ), 'trigger' => sanitize_key( $raw['trigger'] ?? 'completed' ), 'delay_value' => max( 1, min( 90, (int) ( $raw['delay_value'] ?? 7 ) ) ), 'delay_unit' => in_array( $raw['delay_unit'] ?? 'days', array( 'days', 'weeks' ), true ) ? $raw['delay_unit'] : 'days', 'template_id' => sanitize_text_field( $tid ), 'template_name' => sanitize_text_field( $tname ), ) ); } update_option( 'sixrsh_order_sync_enabled', $request->get_param( 'order_sync_enabled' ) ? '1' : '0' ); return new WP_REST_Response( array( 'success' => true, 'message' => __( 'Follow-ups saved.', '6rsh-messaging-notifications' ), ), 200 ); } /** * REST: validate API key and save connection. */ function sixrsh_rest_connect( WP_REST_Request $request ): WP_REST_Response { $key = sanitize_text_field( (string) ( $request->get_param( 'api_key' ) ?? '' ) ); if ( ! $key ) { return new WP_REST_Response( array( 'success' => false, 'message' => 'API key is required.' ), 400 ); } $api = new SixRsh_API( $key ); $result = $api->ping(); if ( ! $result['success'] ) { $error_code = $result['error_code'] ?? ''; return new WP_REST_Response( array( 'success' => false, 'message' => $result['error'] ?: 'Connection failed.', 'code' => $error_code, ), 400 ); } $data = $result['data'] ?? array(); $account = is_array( $data['account'] ?? null ) ? $data['account'] : array(); // Key change → wipe any prior entitlement state so a new key never inherits // the previous account's paid status / callback secret. $prev_key = (string) get_option( 'sixrsh_api_key', '' ); if ( $prev_key !== $key ) { SixRsh_Entitlements::reset(); } // Always save the validated key and account info. 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 the plan.changed callback + prime entitlements (best-effort; the // live check on gated actions is the real freshness guarantee). SixRsh_Entitlements::register_callback(); SixRsh_Entitlements::live(); $phone_connected = ! empty( $account['phone_number'] ); return new WP_REST_Response( array( 'success' => true, 'name' => $account['name'] ?? '', 'phone' => $account['phone_number'] ?? null, 'phone_connected' => $phone_connected, ), 200 ); } /** * REST: run one wizard setup step. */ function sixrsh_rest_setup( WP_REST_Request $request ): WP_REST_Response { $step = sanitize_key( (string) ( $request->get_param( 'step' ) ?? '' ) ); switch ( $step ) { case 'fetch_templates': $api = SixRsh_API::instance(); if ( ! $api ) { return new WP_REST_Response( array( 'success' => false, 'message' => 'No API key configured.' ), 400 ); } $result = $api->get_templates(); if ( ! $result['success'] ) { return new WP_REST_Response( array( 'success' => false, 'message' => $result['error'] ?: 'Failed to fetch templates.' ), 400 ); } $templates = $result['data']['templates'] ?? array(); update_option( 'sixrsh_available_templates', $templates ); return new WP_REST_Response( array( 'success' => true, 'count' => count( $templates ) ) ); case 'match_templates': $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' ), ); $utility_fallback = null; foreach ( $templates as $t ) { if ( strtolower( $t['category'] ?? '' ) === 'utility' ) { $utility_fallback = $t; break; } } 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 && $utility_fallback ) { $matched = $utility_fallback; } if ( $matched ) { $mapping[ $event_key ] = array( 'template_id' => $matched['id'], 'template_name' => $matched['name'], ); } } update_option( 'sixrsh_template_mapping', $mapping ); return new WP_REST_Response( array( 'success' => true, 'mapped' => count( $mapping ) ) ); case 'detect_country': $country = ''; if ( function_exists( 'WC' ) && WC()->countries ) { $country = (string) WC()->countries->get_base_country(); } if ( ! $country ) { $country = 'AE'; } update_option( 'sixrsh_country_code', $country ); return new WP_REST_Response( array( 'success' => true, 'country' => $country ) ); case 'setup_cart': global $wpdb; $table = $wpdb->prefix . 'sixrsh_carts'; // phpcs:disable WordPress.DB.DirectDatabaseQuery,WordPress.DB.PreparedSQL.InterpolatedNotPrepared $wpdb->get_var( "SELECT 1 FROM {$table} LIMIT 1" ); // phpcs:enable if ( $wpdb->last_error ) { SixRsh_Activator::activate(); } return new WP_REST_Response( array( 'success' => true, 'table' => $table ) ); case 'setup_widget': $defaults = SixRsh_Admin::default_settings(); $current = get_option( 'sixrsh_settings', array() ); if ( empty( $current ) ) { update_option( 'sixrsh_settings', $defaults ); } elseif ( empty( $current['widget'] ) ) { $current['widget'] = $defaults['widget']; update_option( 'sixrsh_settings', $current ); } return new WP_REST_Response( array( 'success' => true ) ); default: return new WP_REST_Response( array( 'success' => false, 'message' => 'Unknown step.' ), 400 ); } } /** * REST: mark wizard complete and return summary. */ function sixrsh_rest_complete( WP_REST_Request $request ): WP_REST_Response { update_option( 'sixrsh_setup_banner_show', 1 ); 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'] ) ) ); return new WP_REST_Response( array( 'success' => true, '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&tab=notifications' ), ), 200 ); }