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(
'
',
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;
}
}