0, regardless of online status. */ class AISP_Customer_Activity_Tracker { /** Transient key for active customers cache. */ const TRANSIENT_KEY = 'aisp_ca_active_customers'; /** Transient lifetime in seconds. */ const TRANSIENT_TTL = 60; public static function init() { add_action( 'wp_login', [ __CLASS__, 'on_login' ], 10, 2 ); add_action( 'wp', [ __CLASS__, 'track_activity' ] ); add_action( 'admin_post_aisp_customer_activity_hide', [ __CLASS__, 'hide_customer_callback' ] ); // Invalidate transient when cart changes. add_action( 'woocommerce_cart_updated', [ __CLASS__, 'invalidate_cache' ] ); add_action( 'woocommerce_add_to_cart', [ __CLASS__, 'invalidate_cache' ] ); add_action( 'woocommerce_cart_item_removed', [ __CLASS__, 'invalidate_cache' ] ); add_action( 'woocommerce_cart_emptied', [ __CLASS__, 'invalidate_cache' ] ); // Invalidate dormant cache when order status changes. add_action( 'woocommerce_order_status_changed', [ __CLASS__, 'invalidate_dormant_cache' ] ); add_action( 'woocommerce_new_order', [ __CLASS__, 'invalidate_dormant_cache' ] ); } // ------------------------------------------------------------------------- // Cache // ------------------------------------------------------------------------- public static function invalidate_cache() { delete_transient( self::TRANSIENT_KEY ); } /** * Invalidate the dormant customers cache (called on order changes). * * @since 1.1.0 */ public static function invalidate_dormant_cache() { AISP_Customer_Activity_Dormant::invalidate_cache(); } // ------------------------------------------------------------------------- // Helpers // ------------------------------------------------------------------------- /** * True if the user has at least one tracked role and is not an administrator. */ private static function is_trackable_user( WP_User $user ): bool { $roles = (array) $user->roles; $tracked_roles = AISP_Customer_Activity_Settings::get_tracked_roles(); if ( in_array( 'administrator', $roles, true ) ) { return false; } return ! empty( array_intersect( $roles, $tracked_roles ) ); } // ------------------------------------------------------------------------- // 1. Login // ------------------------------------------------------------------------- public static function on_login( string $user_login, WP_User $user ) { if ( ! self::is_trackable_user( $user ) ) { return; } $now = current_time( 'timestamp' ); update_user_meta( $user->ID, 'aisp_ca_last_login', $now ); update_user_meta( $user->ID, 'aisp_ca_last_activity', $now ); self::invalidate_cache(); } // ------------------------------------------------------------------------- // 2. Page activity + cart // ------------------------------------------------------------------------- public static function track_activity() { if ( ! is_user_logged_in() ) { return; } $user = wp_get_current_user(); if ( ! self::is_trackable_user( $user ) ) { return; } $now = current_time( 'timestamp' ); $last_activity = (int) get_user_meta( $user->ID, 'aisp_ca_last_activity', true ); // Throttle DB writes — update only if older than 60 s. if ( ! $last_activity || ( $now - $last_activity ) >= 60 ) { update_user_meta( $user->ID, 'aisp_ca_last_activity', $now ); self::invalidate_cache(); } self::update_cart_summary( $user->ID, $now ); } // ------------------------------------------------------------------------- // 3. Cart summary // ------------------------------------------------------------------------- public static function update_cart_summary( int $user_id, int $now = 0 ) { if ( ! function_exists( 'WC' ) || ! WC()->cart ) { return; } $now = $now ?: current_time( 'timestamp' ); update_user_meta( $user_id, 'aisp_ca_cart_count', absint( WC()->cart->get_cart_contents_count() ) ); update_user_meta( $user_id, 'aisp_ca_cart_total', wc_format_decimal( WC()->cart->get_cart_contents_total() ) ); update_user_meta( $user_id, 'aisp_ca_cart_updated', $now ); } // ------------------------------------------------------------------------- // 4. Manual hide // ------------------------------------------------------------------------- public static function hide_customer_callback() { if ( ! current_user_can( 'manage_woocommerce' ) ) { wp_die( esc_html__( 'Permission denied.', 'aisp-customer-activity' ) ); } $user_id = isset( $_GET['user_id'] ) ? absint( $_GET['user_id'] ) : 0; if ( ! $user_id ) { wp_safe_redirect( admin_url() ); exit; } check_admin_referer( 'aisp_ca_hide_' . $user_id ); self::delete_user_metas( $user_id ); self::invalidate_cache(); $redirect = ! empty( $_GET['redirect'] ) ? esc_url_raw( wp_unslash( $_GET['redirect'] ) ) : admin_url(); wp_safe_redirect( $redirect ); exit; } // ------------------------------------------------------------------------- // 5. Meta cleanup // ------------------------------------------------------------------------- public static function delete_user_metas( int $user_id ) { foreach ( self::get_meta_keys() as $key ) { delete_user_meta( $user_id, $key ); } } public static function get_meta_keys(): array { return [ 'aisp_ca_last_login', 'aisp_ca_last_activity', 'aisp_ca_cart_count', 'aisp_ca_cart_total', 'aisp_ca_cart_updated', ]; } // ------------------------------------------------------------------------- // 6. Query helpers // ------------------------------------------------------------------------- /** * Return customers active within $hours who have cart > 0. * If $hours === 0, all customers with a cart are returned (no time limit). * Results are cached in a transient for TRANSIENT_TTL seconds. */ public static function get_active_customers( int $hours, int $limit ): array { $cached = get_transient( self::TRANSIENT_KEY ); if ( false === $cached ) { $cached = self::query_active_customers(); set_transient( self::TRANSIENT_KEY, $cached, self::TRANSIENT_TTL ); } // Apply time filter after cache (cache stores all, filter on read). if ( $hours > 0 ) { $limit_time = current_time( 'timestamp' ) - ( $hours * HOUR_IN_SECONDS ); $cached = array_values( array_filter( $cached, function( $user ) use ( $limit_time ) { $last = (int) get_user_meta( $user->ID, 'aisp_ca_last_activity', true ); return $last >= $limit_time; } ) ); } return array_slice( $cached, 0, $limit ); } /** * Raw DB query — all tracked customers with cart > 0, ordered by last activity DESC. * Called only when transient is expired or invalidated. */ private static function query_active_customers(): array { $tracked_roles = AISP_Customer_Activity_Settings::get_tracked_roles(); if ( empty( $tracked_roles ) ) { return []; } $users = get_users( [ 'role__in' => $tracked_roles, 'meta_key' => 'aisp_ca_last_activity', // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key 'meta_compare' => 'EXISTS', 'orderby' => 'meta_value_num', 'order' => 'DESC', 'number' => 2000, 'fields' => [ 'ID', 'display_name', 'user_email' ], ] ); // Filter: cart > 0 only. return array_values( array_filter( $users, function ( $user ) { return (int) get_user_meta( $user->ID, 'aisp_ca_cart_count', true ) > 0; } ) ); } /** * Is the customer currently online? * Uses aisp_ca_online_minutes (default 5). */ public static function is_online( int $user_id ): bool { $last = (int) get_user_meta( $user_id, 'aisp_ca_last_activity', true ); $minutes = (int) get_option( 'aisp_ca_online_minutes', 5 ); return $last && ( current_time( 'timestamp' ) - $last ) <= ( $minutes * 60 ); } /** * Build a nonce-protected hide URL. */ public static function hide_url( int $user_id, string $redirect = '' ): string { $args = [ 'action' => 'aisp_customer_activity_hide', 'user_id' => $user_id, ]; if ( $redirect ) { $args['redirect'] = $redirect; } return wp_nonce_url( admin_url( 'admin-post.php?' . http_build_query( $args ) ), 'aisp_ca_hide_' . $user_id ); } }