type_registry = new FeedbackTypeRegistry(); $this->condition_evaluator = new ConditionEvaluator(); $this->register_feedback_types(); $this->register_conditions(); $this->maybe_upgrade_db(); $this->init_hooks(); } /** * Returns the feedback type registry. * * @return FeedbackTypeRegistry */ public function type_registry(): FeedbackTypeRegistry { return $this->type_registry; } /** * Returns the condition evaluator. * * @return ConditionEvaluator */ public function condition_evaluator(): ConditionEvaluator { return $this->condition_evaluator; } private function register_feedback_types() { $this->type_registry->register( new FeedbackTypes\LikeDislikeType() ); $this->type_registry->register( new FeedbackTypes\EmojiType() ); $this->type_registry->register( new FeedbackTypes\StarRatingType() ); $this->type_registry->register( new FeedbackTypes\NpsType() ); $this->type_registry->register( new FeedbackTypes\TextType() ); $this->type_registry->register( new FeedbackTypes\QuickChoiceType() ); /** * Allows Pro and extensions to register additional feedback types. * * @param FeedbackTypeRegistry $registry The type registry. */ do_action( 'hey_feedback_register_types', $this->type_registry ); } private function register_conditions() { $this->condition_evaluator->register( new Conditions\Core\PostTypeCondition() ); $this->condition_evaluator->register( new Conditions\Core\TaxonomyCondition() ); $this->condition_evaluator->register( new Conditions\Core\UserStatusCondition() ); $this->condition_evaluator->register( new Conditions\Core\DelayCondition() ); /** * Allows Pro and extensions to register additional conditions. * * @param ConditionEvaluator $evaluator The condition evaluator. */ do_action( 'hey_feedback_register_conditions', $this->condition_evaluator ); } private function maybe_upgrade_db() { $db_version = get_option( 'hey_feedback_db_version', '' ); if ( $db_version !== HEY_FEEDBACK_VERSION ) { Database::create_tables(); update_option( 'hey_feedback_db_version', HEY_FEEDBACK_VERSION ); } } private function init_hooks() { add_action( 'init', array( '\HeyFeedback\Shortcode', 'register' ) ); add_action( 'init', array( '\HeyFeedback\Block', 'register' ) ); add_action( 'rest_api_init', array( $this, 'register_rest_routes' ) ); Privacy::register(); if ( is_admin() ) { add_action( 'admin_init', array( $this, 'maybe_redirect_to_onboarding' ) ); add_action( 'admin_menu', array( $this, 'register_admin_menu' ) ); add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_admin_assets' ) ); add_action( 'admin_notices', array( $this, 'maybe_show_review_notice' ) ); } add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_frontend_assets' ) ); add_action( 'wp_footer', array( $this, 'render_widgets' ) ); add_action( 'hey_feedback_send_digest', array( $this, 'send_digest_email' ) ); add_action( 'init', array( $this, 'schedule_digest' ) ); } public function register_rest_routes() { $controllers = array( new RestApi\RespondController(), new RestApi\WidgetsController(), new RestApi\AdminController(), new RestApi\DashboardController(), new RestApi\SettingsController(), ); foreach ( $controllers as $controller ) { $controller->register_routes(); } } public function maybe_redirect_to_onboarding() { if ( ! get_transient( 'hey_feedback_activation_redirect' ) ) { return; } delete_transient( 'hey_feedback_activation_redirect' ); if ( wp_doing_ajax() || wp_doing_cron() ) { return; } global $wpdb; $table = Database::items_table(); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching $count = (int) $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM %i", $table ) ); if ( $count > 0 ) { return; } wp_safe_redirect( admin_url( 'admin.php?page=hey-feedback-welcome' ) ); exit; } public function maybe_show_review_notice() { $screen = get_current_screen(); if ( ! $screen || strpos( $screen->id, 'hey-feedback' ) === false ) { return; } $user_id = get_current_user_id(); if ( get_user_meta( $user_id, 'hey_feedback_reviewed', true ) ) { return; } $remind_after = (int) get_user_meta( $user_id, 'hey_feedback_review_remind_after', true ); if ( $remind_after && time() < $remind_after ) { return; } global $wpdb; $table = Database::responses_table(); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching $count = (int) $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM %i", $table ) ); $milestones = array( 75, 20, 5 ); $current = 0; foreach ( $milestones as $m ) { if ( $count >= $m ) { $current = $m; break; } } if ( ! $current ) { return; } $dismissed = (int) get_user_meta( $user_id, 'hey_feedback_review_dismissed_milestone', true ); if ( $dismissed >= $current ) { return; } $messages = array( 5 => sprintf( /* translators: %d: response count */ __( "You've collected %d responses with Hey! Feedback! If you're enjoying it, a quick review would mean a lot.", 'hey-feedback' ), $count ), 20 => sprintf( /* translators: %d: response count */ __( '%d responses collected! Your review helps other WordPress users discover Hey! Feedback.', 'hey-feedback' ), $count ), 75 => sprintf( /* translators: %d: response count */ __( "Wow, %d responses! You're a feedback pro. A review on WordPress.org would really help us grow.", 'hey-feedback' ), $count ), ); $message = $messages[ $current ]; $review_url = 'https://wordpress.org/support/plugin/hey-feedback/reviews/#new-post'; $rest_url = esc_url_raw( rest_url( 'hey-feedback/v1/admin/review-action' ) ); $nonce = wp_create_nonce( 'wp_rest' ); ?>

$rest_url, 'nonce' => $nonce, 'milestone' => $current, ) ); wp_add_inline_script( 'hey-feedback-review-notice', '(function(){' . 'function send(a){' . 'var el=document.getElementById("hfb-review-notice");' . 'if(el)el.style.display="none";' . 'fetch(heyFeedbackReview.url,{' . 'method:"POST",' . 'headers:{"X-WP-Nonce":heyFeedbackReview.nonce,"Content-Type":"application/json"},' . 'credentials:"same-origin",' . 'body:JSON.stringify({action:a,milestone:heyFeedbackReview.milestone})' . '});' . '}' . 'document.getElementById("hfb-review-rate").addEventListener("click",function(){send("reviewed");});' . 'document.getElementById("hfb-review-later").addEventListener("click",function(e){e.preventDefault();send("later");});' . 'document.getElementById("hfb-review-already").addEventListener("click",function(e){e.preventDefault();send("reviewed");});' . 'var n=document.getElementById("hfb-review-notice");' . 'if(n){' . 'var o=new MutationObserver(function(ml){' . 'ml.forEach(function(m){' . 'if(m.type==="attributes"&&n.style.display==="none"){send("dismiss");o.disconnect();}' . '});' . '});' . 'o.observe(n,{attributes:true,attributeFilter:["style"]});' . '}' . '})();' ); } public function register_admin_menu() { $admin = new Admin\Admin(); $admin->register_menu(); } public function enqueue_admin_assets( $hook_suffix ) { $is_dashboard = 'toplevel_page_hey-feedback' === $hook_suffix; $is_widgets = 'hey-feedback_page_hey-feedback-widgets' === $hook_suffix; $is_settings = 'hey-feedback_page_hey-feedback-settings' === $hook_suffix; $is_onboarding = 'admin_page_hey-feedback-welcome' === $hook_suffix; if ( $is_onboarding ) { $this->enqueue_onboarding_assets(); return; } if ( ! $is_dashboard && ! $is_widgets && ! $is_settings ) { return; } wp_enqueue_style( 'hey-feedback-admin', HEY_FEEDBACK_URL . 'assets/css/admin.css', array(), HEY_FEEDBACK_VERSION ); $types_data = array(); foreach ( $this->type_registry->get_all() as $type ) { $types_data[] = array( 'id' => $type->get_id(), 'label' => $type->get_label(), 'defaultSettings' => $type->get_default_settings(), 'settingsSchema' => $type->get_settings_schema(), ); } $conditions_data = array(); foreach ( $this->condition_evaluator->get_all() as $condition ) { $conditions_data[] = array( 'id' => $condition->get_id(), 'label' => $condition->get_label(), 'settingsSchema' => $condition->get_settings_schema(), ); } $localize_data = array( 'restUrl' => esc_url_raw( rest_url( 'hey-feedback/v1/' ) ), 'adminUrl' => esc_url_raw( admin_url() ), 'nonce' => wp_create_nonce( 'wp_rest' ), 'feedbackTypes' => $types_data, 'conditions' => $conditions_data, ); /** * Filters the admin localize data passed to heyFeedbackAdmin JS object. * * @param array $localize_data Data to localize. * @param string $hook_suffix Current admin page hook suffix. */ $localize_data = apply_filters( 'hey_feedback_admin_localize', $localize_data, $hook_suffix ); if ( $is_dashboard ) { $user_id = get_current_user_id(); $localize_data['showOptinBanner'] = ! get_user_meta( $user_id, 'hey_feedback_subscribed', true ) && ! get_user_meta( $user_id, 'hey_feedback_optin_dismissed', true ); wp_enqueue_script( 'hey-feedback-dashboard', HEY_FEEDBACK_URL . 'assets/js/dashboard.js', array(), HEY_FEEDBACK_VERSION, true ); wp_localize_script( 'hey-feedback-dashboard', 'heyFeedbackAdmin', $localize_data ); } if ( $is_widgets ) { wp_enqueue_style( 'hey-feedback-front', HEY_FEEDBACK_URL . 'assets/css/frontend.css', array(), HEY_FEEDBACK_VERSION ); wp_enqueue_script( 'hey-feedback-admin', HEY_FEEDBACK_URL . 'assets/js/admin.js', array(), HEY_FEEDBACK_VERSION, true ); wp_localize_script( 'hey-feedback-admin', 'heyFeedbackAdmin', $localize_data ); } if ( $is_settings ) { wp_enqueue_script( 'hey-feedback-settings', HEY_FEEDBACK_URL . 'assets/js/settings.js', array(), HEY_FEEDBACK_VERSION, true ); wp_localize_script( 'hey-feedback-settings', 'heyFeedbackAdmin', $localize_data ); } } private function enqueue_onboarding_assets() { $types_data = array(); foreach ( $this->type_registry->get_all() as $type ) { $types_data[] = array( 'id' => $type->get_id(), 'label' => $type->get_label(), 'defaultSettings' => $type->get_default_settings(), 'settingsSchema' => $type->get_settings_schema(), ); } wp_enqueue_style( 'hey-feedback-onboarding', HEY_FEEDBACK_URL . 'assets/css/onboarding.css', array(), HEY_FEEDBACK_VERSION ); wp_enqueue_style( 'hey-feedback-front', HEY_FEEDBACK_URL . 'assets/css/frontend.css', array(), HEY_FEEDBACK_VERSION ); wp_enqueue_script( 'hey-feedback-onboarding', HEY_FEEDBACK_URL . 'assets/js/onboarding.js', array(), HEY_FEEDBACK_VERSION, true ); wp_localize_script( 'hey-feedback-onboarding', 'heyFeedbackOnboarding', array( 'restUrl' => esc_url_raw( rest_url( 'hey-feedback/v1/' ) ), 'nonce' => wp_create_nonce( 'wp_rest' ), 'feedbackTypes' => $types_data, 'widgetsUrl' => esc_url_raw( admin_url( 'admin.php?page=hey-feedback-widgets' ) ), 'dashboardUrl' => esc_url_raw( admin_url( 'admin.php?page=hey-feedback' ) ), ) ); } public function enqueue_frontend_assets() { if ( ! $this->has_active_widgets() ) { return; } wp_enqueue_style( 'hey-feedback-front', HEY_FEEDBACK_URL . 'assets/css/frontend.css', array(), HEY_FEEDBACK_VERSION ); wp_enqueue_script( 'hey-feedback-front', HEY_FEEDBACK_URL . 'assets/js/frontend.js', array(), HEY_FEEDBACK_VERSION, true ); $settings = RestApi\SettingsController::get_all(); $primary_color = $settings['primary_color'] ?? '#2271b1'; wp_add_inline_style( 'hey-feedback-front', '.hey-feedback-widget,.hey-feedback-floating{--hfb-primary:' . esc_attr( $primary_color ) . '}' ); $frontend_config = array( 'restUrl' => esc_url_raw( rest_url( 'hey-feedback/v1/' ) ), 'nonce' => wp_create_nonce( 'wp_rest' ), 'pageId' => get_queried_object_id(), 'pageUrl' => esc_url_raw( home_url( add_query_arg( array() ) ) ), ); /** * Filters the frontend JS configuration object. * * @param array $config Frontend config passed to heyFeedback JS object. */ $frontend_config = apply_filters( 'hey_feedback_frontend_config', $frontend_config ); wp_localize_script( 'hey-feedback-front', 'heyFeedback', $frontend_config ); } /** * Auto-inject widgets via wp_footer based on condition rules. */ public function render_widgets() { if ( ! $this->has_active_widgets() ) { return; } echo '
'; /** * Fires after the feedback root element is rendered in the footer. */ do_action( 'hey_feedback_after_root' ); } /** * Ensures the digest cron event matches the current setting. */ public function schedule_digest() { $settings = RestApi\SettingsController::get_all(); $frequency = $settings['digest_frequency'] ?? 'off'; $hook = 'hey_feedback_send_digest'; $scheduled = wp_get_schedule( $hook ); if ( 'off' === $frequency ) { if ( $scheduled ) { wp_clear_scheduled_hook( $hook ); } return; } if ( $scheduled !== $frequency ) { wp_clear_scheduled_hook( $hook ); wp_schedule_event( time(), $frequency, $hook ); } } /** * Sends a digest email summarising responses since the last digest. */ public function send_digest_email() { $settings = RestApi\SettingsController::get_all(); $frequency = $settings['digest_frequency'] ?? 'off'; if ( 'off' === $frequency ) { return; } $last_sent = get_option( 'hey_feedback_last_digest', 0 ); $since = $last_sent ? gmdate( 'Y-m-d H:i:s', $last_sent ) : gmdate( 'Y-m-d H:i:s', strtotime( '-1 week' ) ); global $wpdb; $responses_table = Database::responses_table(); $items_table = Database::items_table(); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching $rows = $wpdb->get_results( $wpdb->prepare( "SELECT i.title AS widget_title, i.id AS widget_id, COUNT(*) AS cnt FROM %i r INNER JOIN %i i ON i.id = r.widget_id WHERE r.created_at > %s GROUP BY r.widget_id ORDER BY cnt DESC", $responses_table, $items_table, $since ) ); if ( empty( $rows ) ) { update_option( 'hey_feedback_last_digest', time() ); return; } $total = 0; $lines = array(); foreach ( $rows as $row ) { $title = $row->widget_title ?: ( __( 'Widget', 'hey-feedback' ) . ' #' . $row->widget_id ); $lines[] = sprintf( ' %s: %d', $title, $row->cnt ); $total += (int) $row->cnt; } $blog = get_bloginfo( 'name' ); $period = 'daily' === $frequency ? __( 'daily', 'hey-feedback' ) : __( 'weekly', 'hey-feedback' ); $subject = sprintf( /* translators: 1: site name, 2: total count */ __( '[%1$s] Feedback digest — %2$d new responses', 'hey-feedback' ), $blog, $total ); $body = sprintf( /* translators: 1: period (daily/weekly), 2: site name, 3: response lines */ __( "%1\$s feedback digest for %2\$s\n\n%3\$s\n\nTotal: %4\$d", 'hey-feedback' ), ucfirst( $period ), $blog, implode( "\n", $lines ), $total ); wp_mail( get_option( 'admin_email' ), $subject, $body ); update_option( 'hey_feedback_last_digest', time() ); } /** * Checks whether any active widgets exist. Result is cached per request. * * @return bool */ private function has_active_widgets(): bool { static $has = null; if ( null !== $has ) { return $has; } global $wpdb; $table = Database::items_table(); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching $count = (int) $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM %i WHERE status = %s", $table, 'active' ) ); $has = $count > 0; return $has; } }