= $offer['base_minor'] || $offer['credits'] > 16777215 ) { return false; } } if ( isset( $state['consent_version'] ) && ! is_string( $state['consent_version'] ) ) { return false; } if ( isset( $state['preferences'] ) ) { if ( ! is_array( $state['preferences'] ) ) { return false; } foreach ( $state['preferences'] as $key => $value ) { if ( in_array( $key, array( 'country', 'email' ), true ) ? ! is_string( $value ) : ! is_int( $value ) ) { return false; } } } return true; } /** * Preserve validated activation integers while sanitizing stored text. * * @param mixed $value Scalar activation value. * @return bool|int|string Sanitized value with boolean and integer types retained. */ function ai4seo_sanitize_activation_state_value( $value ) { return is_int( $value ) || is_bool( $value ) ? $value : sanitize_text_field( $value ); } /** * Store only valid server state, clearing offers no longer exposed by the server. * * @param mixed $data Untrusted account payload. */ function ai4seo_store_activation_data( $data ): void { $data = is_array( $data ) ? $data : array( 'available' => false ); $data['received_at'] = time(); $data['account'] = ai4seo_get_notification_preferences_account(); ai4seo_update_environmental_variable( AI4SEO_ENVIRONMENTAL_VARIABLE_ACTIVATION, ai4seo_validate_activation_state( $data ) ? $data : array() ); } /** * Reject stale personal marketing; a failed sync never extends a previous permission snapshot. * * @param bool $preferences_only Allow older preference snapshots without returning their offers. * @return array Valid account-bound snapshot, or an empty array when unavailable. */ function ai4seo_get_activation_data( bool $preferences_only = false ): array { $data = ai4seo_read_environmental_variable( AI4SEO_ENVIRONMENTAL_VARIABLE_ACTIVATION ); if ( ! ai4seo_validate_activation_state( $data ) || ( ! $preferences_only && time() - ( $data['received_at'] ?? 0 ) > 600 ) || ( $data['received_at'] ?? 0 ) > time() || ( isset( $data['account'] ) && ai4seo_get_notification_preferences_account() !== $data['account'] ) ) { return array(); } if ( $preferences_only ) { unset( $data['offer'] ); } return $data; } /** Respect site-wide dismissal even when the remote payload still contains the same cycle. */ function ai4seo_get_activation_offer(): array { $sync = ai4seo_read_environmental_variable( AI4SEO_ENVIRONMENTAL_VARIABLE_NOTIFICATION_PREFERENCES ); if ( ! empty( $sync['pending'] ) || ( $sync && ( $sync['account'] ?? '' ) !== ai4seo_get_notification_preferences_account() ) ) { return array(); } $data = ai4seo_get_activation_data(); $offer = $data['offer'] ?? array(); if ( ! $offer || $offer['expires_at'] <= time() || ! empty( $data['preferences']['opted_out'] ) || ai4seo_is_notification_dismissed( 'activation-' . $offer['id'] ) ) { return array(); } return $offer; } /** * Render a quote consistently on Account, Dashboard, and credit selection. * * @param array $offer Validated immutable offer. * @param bool $global_notice Whether to render the compact global notice. */ function ai4seo_render_activation_offer( array $offer, bool $global_notice = false ): void { if ( ! ai4seo_can_administer_plugin() ) { return; } if ( $global_notice ) { echo '

'; /* translators: 1: Plugin name, 2: Credits, 3: Price, 4: Currency. */ echo esc_html( sprintf( __( 'Personal %1$s offer: %2$s credits for %3$s %4$s.', 'ai-for-seo' ), AI4SEO_PLUGIN_NAME, number_format_i18n( $offer['credits'] ), number_format_i18n( $offer['net_minor'] / 100, 2 ), strtoupper( $offer['currency'] ) ) ); echo ' ' . esc_html( 'exclusive' === $offer['tax_behavior'] ? __( 'Plus applicable tax.', 'ai-for-seo' ) : __( 'Including applicable tax.', 'ai-for-seo' ) ); echo '

'; echo '' . esc_html__( 'View offer', 'ai-for-seo' ) . ''; ai4seo_echo_wp_kses( ai4seo_get_offer_preference_button( 'admin' ) ); echo '
'; // Supply the dismiss control so WordPress does not remove the notice before the server confirms it. echo '
'; return; } echo '

'; /* translators: 1: Number of credits, 2: Price, 3: Currency. */ echo esc_html( sprintf( __( 'Your personal offer: %1$s credits for %2$s %3$s', 'ai-for-seo' ), number_format_i18n( $offer['credits'] ), number_format_i18n( $offer['net_minor'] / 100, 2 ), strtoupper( $offer['currency'] ) ) ); echo '

'; /* translators: 1: Discount percentage, 2: Expiration date in the site timezone. */ echo esc_html( sprintf( __( '%1$d%% off the proportional 5,000-credit package price. Includes your reported open credit requirement plus 5%%. Valid until %2$s.', 'ai-for-seo' ), $offer['percentage'], wp_date( 'Y-m-d H:i T', $offer['expires_at'] ) ) ); echo ' ' . esc_html( 'exclusive' === $offer['tax_behavior'] ? __( 'Applicable tax is added in checkout.', 'ai-for-seo' ) : __( 'Includes applicable tax.', 'ai-for-seo' ) ); echo '

' . esc_html( $offer['code'] ) . ' ' . esc_html__( 'Applied automatically in checkout.', 'ai-for-seo' ) . '

'; foreach ( array( 'buy' => __( 'Buy this offer', 'ai-for-seo' ), 'dismiss' => __( 'Hide this offer', 'ai-for-seo' ), ) as $action => $label ) { $button_class = 'buy' === $action ? 'ai4seo-button ai4seo-unicorn-button' : 'ai4seo-button ai4seo-secondary-button'; echo ' '; } ai4seo_echo_wp_kses( ai4seo_get_offer_preference_button( 'plugin' ) ); echo '
'; } /** * Show current offers, including explicitly requested email offers on the Account page. * * @param bool $preferences Whether this is the Account page, including requested email offers. */ function ai4seo_render_activation_area( bool $preferences = false ): void { if ( ! ai4seo_can_administer_plugin() ) { return; } $offer = ai4seo_get_activation_offer(); if ( ! ai4seo_get_setting( AI4SEO_SETTING_SHOW_PLUGIN_OFFERS ) ) { $offer = array(); } // An email link explicitly requests this one quote; it does not enable plugin marketing. // phpcs:disable WordPress.Security.NonceVerification.Recommended -- Read-only quote link; owner authentication remains mandatory at the API. if ( $preferences && isset( $_GET['activation_offer'] ) && is_string( $_GET['activation_offer'] ) ) { $requested = sanitize_text_field( wp_unslash( $_GET['activation_offer'] ) ); if ( 1 === preg_match( '/^[a-f0-9]{32}$/D', $requested ) ) { // An explicit quote must not fall back to a different cached offer after a failed lookup. $offer = array(); $response = ai4seo_robhub_api()->call( 'client/activation-preferences', array( 'preferences' => wp_json_encode( array( 'action' => 'view', 'offer_id' => $requested, ) ), ) ); $view = array( 'available' => false, 'received_at' => time(), 'offer' => $response['data']['offer'] ?? null, ); if ( ai4seo_robhub_api()->was_call_successful( $response ) && is_array( $view['offer'] ) && ai4seo_validate_activation_state( $view ) && $requested === $view['offer']['id'] && $view['offer']['expires_at'] > time() ) { $offer = $view['offer']; } } } // phpcs:enable WordPress.Security.NonceVerification.Recommended if ( $offer ) { ai4seo_render_activation_offer( $offer ); } } /** Render voluntary channel preferences within the Account privacy section. */ function ai4seo_render_activation_preferences(): void { if ( ! ai4seo_can_administer_plugin() ) { return; } $data = ai4seo_get_activation_data( true ); $permission = $data['preferences'] ?? array(); $original = array( 'email' => ! empty( $permission['email_consented_at'] ), 'country' => (string) ( $permission['country'] ?? '' ), 'address' => (string) ( $permission['email'] ?? '' ), 'consent_version' => (string) ( $data['consent_version'] ?? '' ), 'expected_revision' => (int) ( $permission['preferences_revision'] ?? 0 ), ); echo '
'; echo ''; echo ''; echo ''; echo '
'; echo '' . esc_html__( 'Optional email communication', 'ai-for-seo' ) . ':'; echo '
'; /* translators: %s: Plugin name. */ echo ''; echo '

' . esc_html__( 'Offers may be personalized using this website’s reported credit requirements. You can withdraw your email consent at any time without affecting plugin access.', 'ai-for-seo' ); echo ' ' . esc_html__( 'More information:', 'ai-for-seo' ) . ' ' . esc_html__( 'Privacy policy', 'ai-for-seo' ) . '

'; echo '
'; echo ''; echo '
'; echo '
'; echo ''; echo '
'; if ( ! empty( $permission['email_consented_at'] ) && empty( $permission['email_confirmed_at'] ) ) { echo '

' . esc_html__( 'Please confirm your email address using the link in the confirmation email.', 'ai-for-seo' ) . '

'; } echo '
'; if ( ! empty( $permission['opted_out'] ) ) { echo '
' . esc_html__( 'Personal offers', 'ai-for-seo' ) . ':
'; echo '
'; } echo '
'; } /** * Bind a rendered preference snapshot to its license, including across account changes. * * @param array $preferences Original form values. * @return string Account-bound signature without exposing credentials. */ function ai4seo_get_activation_preferences_signature( array $preferences ): string { return wp_hash( wp_json_encode( array( $preferences, ai4seo_robhub_api()->read_auth_data() ) ) ); } /** A global notice only for an authorized administrator with a current personal offer. */ function ai4seo_activation_admin_notice(): void { if ( ! ai4seo_can_administer_plugin() || ai4seo_is_plugin_page_active() ) { return; } $offer = ai4seo_get_activation_offer(); if ( $offer ) { ai4seo_render_activation_offer( $offer, true ); } } /** Add one non-forced notification per cycle; shared read state has no pricing effect. */ function ai4seo_check_activation_notification(): void { $offer = ai4seo_get_activation_offer(); if ( ! $offer ) { return; } /* translators: %s: Website-specific offer code. */ $message = sprintf( __( 'A personal credit offer is available: %s. Open your account to view the quantity and price.', 'ai-for-seo' ), $offer['code'] ); ai4seo_push_notification( 'activation-' . $offer['id'], $message, false, array( 'activation_offer_id' => $offer['id'], 'type' => 'info', 'get_more_credits_button' => true, ) ); } /** Administrative AJAX: separate consent, dismissal, and global opt-out actions. */ function ai4seo_update_activation_preferences(): void { if ( ! ai4seo_singleton( __FUNCTION__ ) || ! ai4seo_require_ajax_administration() ) { return; } if ( false === wp_verify_nonce( $GLOBALS['ai4seo_ajax_nonce'] ?? '', AI4SEO_GLOBAL_NONCE_IDENTIFIER ) ) { ai4seo_send_ajax_error( esc_html__( 'Please refresh the page and try again.', 'ai-for-seo' ), 5092704 ); return; } // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- JSON booleans retain their types; the authenticated API validates every allowed field. $input = isset( $_POST['preferences'] ) && is_string( $_POST['preferences'] ) ? json_decode( wp_unslash( $_POST['preferences'] ), true ) : null; if ( ! is_array( $input ) ) { ai4seo_send_ajax_error( esc_html__( 'Invalid offer preferences.', 'ai-for-seo' ), 5092702 ); return; } $result = ai4seo_save_activation_preferences( $input ); if ( is_wp_error( $result ) ) { ai4seo_send_ajax_error( $result->get_error_message(), (int) $result->get_error_code() ); return; } ai4seo_send_ajax_success(); } /** * Share the authenticated API update between Account saves and offer actions. * * @param array $input Channel choices or an explicit offer action. * @return WP_Error|null Error on failure, null after confirmed persistence. */ function ai4seo_save_activation_preferences( array $input ) { if ( ! ai4seo_can_administer_plugin() ) { return new WP_Error( 11420725, esc_html__( 'Action blocked due to security reasons. Please refresh this page and try again.', 'ai-for-seo' ) ); } $input['actor'] = (string) get_current_user_id(); $response = ai4seo_robhub_api()->call( 'client/activation-preferences', array( 'preferences' => wp_json_encode( $input ) ) ); if ( ! ai4seo_robhub_api()->was_call_successful( $response ) ) { return new WP_Error( 5092703, esc_html__( 'Could not save offer preferences. Please try again later.', 'ai-for-seo' ) ); } if ( 'dismiss' === ( $input['action'] ?? '' ) && is_string( $input['offer_id'] ?? null ) ) { ai4seo_mark_notification_as_dismissed( 'activation-' . $input['offer_id'] ); } ai4seo_store_activation_data( $response['data'] ?? array() ); return null; }