notices[ $id ] = array( 'id' => $id, 'message' => $message, 'type' => $type, 'dismissible' => (bool) $dismissible, ); } /** * Render all queued notices that have not been dismissed. * * Hooked to admin_notices. */ public function show() { if ( empty( $this->notices ) ) { return; } foreach ( $this->notices as $notice ) { if ( $this->is_dismissed( $notice['id'] ) ) { continue; } $classes = 'notice notice-' . esc_attr( $notice['type'] ) . ' ai-schema-gen-notice'; if ( $notice['dismissible'] ) { $classes .= ' is-dismissible'; } printf( '

%3$s

', esc_attr( $classes ), esc_attr( $notice['id'] ), wp_kses_post( $notice['message'] ) ); } } /** * Dismiss a notice for the current user. * * @param string $id The notice ID to dismiss. */ public function dismiss( $id ) { $user_id = get_current_user_id(); $dismissed = $this->get_dismissed_notices( $user_id ); if ( ! in_array( $id, $dismissed, true ) ) { $dismissed[] = $id; update_user_meta( $user_id, self::DISMISSED_META_KEY, $dismissed ); } } /** * Check whether a notice has been dismissed by the current user. * * @param string $id The notice ID. * @return bool True if the notice has been dismissed. */ public function is_dismissed( $id ) { $user_id = get_current_user_id(); $dismissed = $this->get_dismissed_notices( $user_id ); return in_array( $id, $dismissed, true ); } /** * AJAX handler for dismissing a notice. * * Expects a POST request with 'notice_id' and the standard nonce. */ public function ajax_dismiss() { check_ajax_referer( self::AJAX_ACTION, 'nonce' ); if ( ! isset( $_POST['notice_id'] ) ) { wp_send_json_error( array( 'message' => 'Missing notice_id.' ) ); } $notice_id = sanitize_text_field( wp_unslash( $_POST['notice_id'] ) ); $this->dismiss( $notice_id ); wp_send_json_success( array( 'dismissed' => $notice_id ) ); } /** * Enqueue the inline JavaScript for AJAX dismiss handling. * * Only loads on admin pages. */ public function enqueue_scripts() { if ( ! is_admin() ) { return; } $nonce = wp_create_nonce( self::AJAX_ACTION ); $script = '(function($){' . "\n"; $script .= ' $(document).on(\'click\', \'.ai-schema-gen-notice .notice-dismiss\', function(){' . "\n"; $script .= ' var noticeId = $(this).closest(\'.ai-schema-gen-notice\').data(\'notice-id\');' . "\n"; $script .= ' if ( ! noticeId ) return;' . "\n"; $script .= ' $.post(ajaxurl, {' . "\n"; $script .= ' action: \'' . esc_js( $this->get_ajax_action() ) . '\',' . "\n"; $script .= ' notice_id: noticeId,' . "\n"; $script .= ' nonce: \'' . esc_js( $nonce ) . '\'' . "\n"; $script .= ' });' . "\n"; $script .= ' });' . "\n"; $script .= '})(jQuery);'; wp_add_inline_script( 'jquery-core', $script ); } /** * Register the built-in notifications. * * Called on admin_init so that all plugin data is available. */ public function register_built_in_notices() { // 1. API key missing. $this->maybe_add_api_key_missing(); // 2. Update available. $this->maybe_add_update_available(); // 3. Rate limit warning. $this->maybe_add_rate_limit_warning(); // 4. Welcome notice after first activation. $this->maybe_add_welcome(); } /** * Show an error notice when no API key is configured. */ private function maybe_add_api_key_missing() { $settings = get_option( 'schema_ai_settings', array() ); $api_key = isset( $settings['api_key'] ) ? $settings['api_key'] : ''; // Also check per-user API key. $user_key = get_user_meta( get_current_user_id(), 'schema_ai_api_key', true ); if ( empty( $api_key ) && empty( $user_key ) ) { $this->add( 'api_key_missing', sprintf( /* translators: 1: URL to setup wizard, 2: URL to registration page. */ __( 'AI Schema Gen: Register for a free account to get your API key and start generating schema markup with AI. Already have an account? Run the setup wizard.', 'ai-schema-gen' ), admin_url( 'admin.php?page=ai-schema-gen-setup' ) ), 'warning', true ); } } /** * Show an info notice when a plugin update is available. */ private function maybe_add_update_available() { if ( ! current_user_can( 'update_plugins' ) ) { return; } $update_plugins = get_site_transient( 'update_plugins' ); if ( ! $update_plugins || ! isset( $update_plugins->response ) ) { return; } $plugin_basename = defined( 'SCHEMA_AI_PLUGIN_BASENAME' ) ? SCHEMA_AI_PLUGIN_BASENAME : ''; if ( ! empty( $plugin_basename ) && isset( $update_plugins->response[ $plugin_basename ] ) ) { $new_version = $update_plugins->response[ $plugin_basename ]->new_version; $this->add( 'update_available', sprintf( /* translators: 1: New version number. 2: URL to plugins page. */ __( 'AI Schema Gen: Version %1$s is available. Update now to get the latest features and fixes.', 'ai-schema-gen' ), esc_html( $new_version ), admin_url( 'plugins.php' ) ), 'info', true ); } } /** * Show a warning when the current user is approaching their daily limit. * * Triggers when the user has used 80% or more of their monthly quota. */ private function maybe_add_rate_limit_warning() { $user_id = get_current_user_id(); if ( ! $user_id ) { return; } $plan = get_user_meta( $user_id, 'schema_ai_plan', true ); // Only relevant for free users. Backend plans: free, pro, premium. if ( 'premium' === $plan || 'pro' === $plan ) { return; } global $wpdb; $table = esc_sql( $wpdb->prefix . 'schema_ai_usage' ); $month_start = gmdate( 'Y-m-01' ); $limit = 30; // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Required for custom table; no WP API alternative exists. $count = (int) $wpdb->get_var( $wpdb->prepare( // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Table name is sanitized via esc_sql() and not user input. "SELECT COALESCE( SUM( ai_generations ), 0 ) FROM {$table} WHERE user_id = %d AND date >= %s", $user_id, $month_start ) ); // Warn at 80% usage (24 out of 30). if ( $count >= ( $limit * 0.8 ) ) { $remaining = max( 0, $limit - $count ); $this->add( 'rate_limit_warning', sprintf( /* translators: 1: Remaining count, 2: URL to upgrade page. */ __( 'AI Schema Gen: You have %1$d AI generation(s) remaining this month. Upgrade your plan for more generations.', 'ai-schema-gen' ), $remaining, admin_url( 'admin.php?page=ai-schema-gen-pricing' ) ), 'warning', true ); } } /** * Show a welcome notice after first activation. */ private function maybe_add_welcome() { $activation_time = get_option( 'schema_ai_activation_time', 0 ); // Only show for the first 7 days after activation. if ( empty( $activation_time ) || ( time() - (int) $activation_time ) > WEEK_IN_SECONDS ) { return; } $this->add( 'welcome', sprintf( /* translators: %s: URL to the getting-started page. */ __( 'Welcome to AI Schema Gen! Thanks for installing AI Schema Gen. Get started by configuring your settings and generating your first schema markup.', 'ai-schema-gen' ), admin_url( 'admin.php?page=ai-schema-gen' ) ), 'success', true ); } /** * Get the list of dismissed notice IDs for a user. * * @param int $user_id WordPress user ID. * @return array List of dismissed notice ID strings. */ private function get_dismissed_notices( $user_id ) { $dismissed = get_user_meta( $user_id, self::DISMISSED_META_KEY, true ); if ( ! is_array( $dismissed ) ) { return array(); } return $dismissed; } /** * Return the AJAX action string. Helper for the inline script. * * @return string */ private function get_ajax_action() { return self::AJAX_ACTION; } }