..., 'at'=>ts] of the first "it worked" moment public static function register() { add_action('admin_init', array(__CLASS__, 'stamp_first_seen')); add_action('admin_notices', array(__CLASS__, 'maybe_render')); add_action('wp_ajax_' . self::AJAX_ACTION, array(__CLASS__, 'ajax_handle')); } public static function stamp_first_seen() { if (!get_option(self::FIRST_SEEN_OPT)) { update_option(self::FIRST_SEEN_OPT, time(), false); } } /** * Record the first moment the plugin visibly did something for this site * (first approved fix applied, first scan that returned a score). The * nudge then shows on the next AllAccessible page regardless of install * age. Stamped once; later calls are no-ops. */ public static function mark_moment(string $kind): void { if (get_option(self::MOMENT_OPT)) return; update_option(self::MOMENT_OPT, array('kind' => sanitize_key($kind), 'at' => time()), false); } /** * Should the nudge show right now? */ private static function should_show(): bool { if (!current_user_can('manage_options')) return false; // Screen gate first — only AllAccessible pages + Plugins + Dashboard. // Checked before any option reads so unrelated admin pages don't // pay for three non-autoloaded option queries. $screen = function_exists('get_current_screen') ? get_current_screen() : null; $id = $screen ? (string) $screen->id : ''; $allowed = (strpos($id, 'allaccessible') !== false) || in_array($id, array('plugins', 'dashboard'), true); if (!$allowed) return false; // State check: done forever, or snoozed into the future. $state = get_option(self::OPTION, array()); if (!empty($state['done'])) return false; if (!empty($state['snooze_until']) && time() < (int) $state['snooze_until']) return false; // Age gate, unless the plugin has already delivered a visible win // (first approved fix / first scored scan) — then ask right away. $moment = get_option(self::MOMENT_OPT); if (empty($moment['at'])) { $first = (int) get_option(self::FIRST_SEEN_OPT, 0); if ($first === 0) return false; // not stamped yet if (time() - $first < self::MIN_AGE_DAYS * DAY_IN_SECONDS) return false; } return true; } public static function maybe_render() { if (!self::should_show()) return; $nonce = wp_create_nonce(self::AJAX_ACTION); $ajax = admin_url('admin-ajax.php'); ?>

'forbidden'), 403); } check_ajax_referer(self::AJAX_ACTION); $act = isset($_POST['nudge_act']) ? sanitize_key($_POST['nudge_act']) : ''; $state = (array) get_option(self::OPTION, array()); switch ($act) { case 'sentiment': // Record sentiment for analytics; don't mark done yet — // the user still has a step-2 action to take. $state['sentiment'] = sanitize_key($_POST['sentiment'] ?? ''); $state['sentiment_at'] = time(); $moment = get_option(self::MOMENT_OPT); $state['moment_kind'] = is_array($moment) ? ($moment['kind'] ?? '') : ''; break; case 'review': // clicked the wp.org review link case 'done': // "I already did" $state['done'] = true; break; case 'feedback': $state['done'] = true; $comment = isset($_POST['comment']) ? sanitize_textarea_field($_POST['comment']) : ''; self::relay_feedback($comment); break; case 'snooze': $state['snooze_until'] = time() + self::SNOOZE_DAYS * DAY_IN_SECONDS; break; default: wp_send_json_error(array('message' => 'unknown action'), 400); } update_option(self::OPTION, $state, false); wp_send_json_success(array('act' => $act)); } /** * Relay review-nudge feedback to the existing AllAccessible feedback * endpoint — same one DeactivationSurvey uses, same form shape */ private static function relay_feedback(string $comment) { if ($comment === '') return; if (!function_exists('wp_remote_post')) return; $plugin_data = wp_json_encode(array( 'version' => defined('AACB_VERSION') ? AACB_VERSION : 'unknown', 'url' => get_bloginfo('url'), )); wp_remote_post('https://app.allaccessible.org/api/wp-feedback', array( 'body' => array( 'type' => 'review_nudge', 'reason' => 'could_be_better', 'plugin' => $plugin_data, 'comment-could_be_better' => $comment, ), 'headers' => array('Content-Type' => 'application/x-www-form-urlencoded'), 'timeout' => 5, 'blocking' => false, )); } }