# wpfunnels/3.13.3/admin/modules/settings/class-wpfnl-settings.php

WPFunnels – Funnel Builder for WooCommerce with Checkout &amp; One Click Upsell, version 3.13.3. 1,079 lines.

- Page: https://pluginprobe.com/plugins/wpfunnels/3.13.3/code/admin/modules/settings/class-wpfnl-settings.php
- Raw: https://pluginprobe.com/plugins/wpfunnels/3.13.3/raw/admin/modules/settings/class-wpfnl-settings.php
- Modified: 2026-09-30T09:56:46+00:00

Line numbers below start at 1. Link to a line or a range by appending a fragment to the
page URL, for example `https://pluginprobe.com/plugins/wpfunnels/3.13.3/code/admin/modules/settings/class-wpfnl-settings.php#L10-L20`.

```php
<?php
/**
 * Settings module
 *
 * @package
 */

namespace WPFunnels\Modules\Admin\Settings;

use WPFunnels\Admin\Module\Wpfnl_Admin_Module;
use WPFunnels\Rollback;
use WPFunnels\Traits\SingletonTrait;
use WPFunnels\Wpfnl_functions;

class Module extends Wpfnl_Admin_Module
{
    use SingletonTrait;

    protected $validations;

    protected $prefix = '_wpfunnels_';

    public function __construct()
    {
        add_action('admin_init', [$this, 'after_permalink_settings_saved']);
		add_action( 'admin_post_wpfunnels_rollback', [ $this, 'post_wpfunnels_rollback' ] );
		add_action( 'wpfunnels_send_revenue_report_email', [ $this, 'send_revenue_report_email' ] );
        $this->init_ajax();
    }

    /**
     * Render the settings screen.
     *
     * The screen is a route of the React admin now — `?page=wpfunnels#/settings`
     * — which admin_init sends this page to (see
     * Wpfnl_Menus::maybe_redirect_to_react_admin). Printing the app's root here
     * keeps the page working for anyone who filters that redirect off; there is
     * no server-rendered settings markup any more.
     *
     * @return void
     * @since  1.0.0
     */
    public function get_view()
    {
        /*
         * The app reads its route from the fragment, which never reaches the
         * server — so landing here with no fragment would open the dashboard.
         * The bundle is a footer script, so this still prints before it runs.
         */
        wp_add_inline_script(
            'wpfnl-react-admin',
            "if ( ! window.location.hash ) { window.location.hash = '#/settings'; }",
            'before'
        );

        require WPFNL_ADMIN_DIR . 'views/react-admin.php';
    }

    /**
     * Init ajax hooks for
     * saving metas
     *
     * @since 1.0.0
     */
    public function init_ajax()
    {
        /*
         * Only the two actions that are still posted to from outside this module
         * are registered: the Vue template library's sync button
         * (`clear-templates`) and the setup wizard (`update-general-settings`).
         * Everything the settings screen itself used to post goes through
         * wpfunnels/v1/settings/* now — see
         * includes/core/rest-api/Controllers/class-settings-page-controller.php,
         * which calls the very same handlers below.
         */
        $this->validations = [
            'logged_in' => true,
            'user_can' => 'wpf_manage_funnels',
        ];
        wp_ajax_helper()->handle('update-general-settings')
            ->with_callback([ $this, 'update_general_settings' ])
            ->with_validation($this->validations);

        wp_ajax_helper()->handle('clear-templates')
            ->with_callback([ $this, 'clear_templates_data' ])
            ->with_validation($this->validations);

    }


    /**
     * Update handler for settings
     * page
     *
     * @param $payload
     *
     * @return array
     * @since  1.0.0
     */
    public function update_general_settings($payload)
    {
        do_action('wpfunnels/before_settings_saved', $payload);

        $this->save_ai_settings($payload);
        $existing_settings = Wpfnl_functions::get_general_settings();
        $general_settings  = [
            'builder'                   => sanitize_text_field($payload['builder']),
            'uninstall_cleanup'         => isset($payload['uninstall_cleanup']) ? sanitize_text_field($payload['uninstall_cleanup']) : 'off',
            'disable_analytics'         => isset($payload['analytics_roles']) ? $payload['analytics_roles'] : '',
            'allow_funnels'             => isset($payload['permission_role']) ? $payload['permission_role'] : [],
            'paypal_reference'          => isset($payload['paypal_reference']) ? sanitize_text_field($payload['paypal_reference']) : 'off',
            'order_bump'                => isset($payload['order_bump']) ? sanitize_text_field($payload['order_bump']) : 'off',
            'ab_testing'                => isset($payload['ab_testing']) ? sanitize_text_field($payload['ab_testing']) : 'off',
            'disable_theme_style'       => isset($payload['disable_theme_style']) ? sanitize_text_field($payload['disable_theme_style']) : 'off',
            'enable_log_status'         => isset($payload['enable_log_status']) ? sanitize_text_field($payload['enable_log_status']) : 'off',
            'enable_skip_cart'          => isset($payload['enable_skip_cart']) ? $payload['enable_skip_cart'] : 'no',
            'skip_cart_for'             => isset($payload['skip_cart_for']) ? $payload['skip_cart_for'] : 'whole',
            'funnel_builder_mode'       => isset($payload['funnel_builder_mode']) ? sanitize_text_field($payload['funnel_builder_mode']) : Wpfnl_functions::get_default_funnel_builder_mode(),
        ];

        if ( isset( $payload['funnel_builder_mode'] ) ) {
            // Choosing the site default here is an explicit statement of intent, so
            // drop this user's own canvas override — otherwise a view they toggled
            // earlier would keep winning over the mode they just picked.
            delete_user_meta( get_current_user_id(), Wpfnl_functions::CANVAS_VIEW_MODE_META_KEY );

            if ( 'vertical' === $payload['funnel_builder_mode'] ) {
                do_action( 'wpfunnels_vertical_mode_used' );
            }
        }

        $permalink_settings = [
            'structure'   => isset($payload['permalink_settings']) ? sanitize_text_field($payload['permalink_settings']) : 'default',
            'step_base'   => isset($payload['permalink_step_base']) ? sanitize_text_field($payload['permalink_step_base']) : WPFNL_STEPS_POST_TYPE,
            'funnel_base' => isset($payload['permalink_funnel_base']) ? sanitize_text_field($payload['permalink_funnel_base']) : WPFNL_FUNNELS_POST_TYPE,
        ];
        
        if( isset($payload['sender_name']) && isset($payload['sender_email']) ){
            $optin_settings = [
                'sender_name'   => sanitize_text_field($payload['sender_name']),
                'sender_email'  => sanitize_text_field($payload['sender_email']),
                'email_subject' => isset($payload['email_subject']) ? sanitize_text_field($payload['email_subject']) : '',
            ];
        }else{
            $optin_settings = Wpfnl_functions::get_optin_settings();
        }
       

        foreach ($payload as $key => $value) {
            switch ($key) {
                case 'builder':
                    $cache_key = 'wpfunnels_remote_template_data_' . WPFNL_VERSION;
                    delete_transient($cache_key);
                    delete_option(WPFNL_TEMPLATES_OPTION_KEY);
                    break;
                case 'permalink_settings':
                    Wpfnl_functions::update_admin_settings($this->prefix.'permalink_saved', true);
                    break;
                case 'advanced_settings':
                    Wpfnl_functions::update_admin_settings($this->prefix.'advanced_settings', $value);
                    break;
                default:
                    break;
            }
        }

        Wpfnl_functions::update_admin_settings($this->prefix.'general_settings', $general_settings);
        Wpfnl_functions::update_admin_settings($this->prefix.'permalink_settings', $permalink_settings);
        Wpfnl_functions::update_admin_settings($this->prefix.'optin_settings', $optin_settings);
		$this->save_recaptcha_settings($payload);
		$this->save_tracking_consent($payload);
		$this->save_user_role_management_data($payload);
		$this->save_google_map_key($payload);
		$this->save_lms_settings($payload);
		$this->save_notification_settings($payload);

        delete_option(WPFNL_TEMPLATES_OPTION_KEY.'_74');
        delete_option(WPFNL_TEMPLATES_OPTION_KEY.'_73');
        delete_option(WPFNL_TEMPLATES_OPTION_KEY.'_52');
        delete_option(WPFNL_TEMPLATES_OPTION_KEY.'_53');

        delete_transient('wpfunnels_remote_template_data_74_' . WPFNL_VERSION);
        delete_transient('wpfunnels_remote_template_data_73_' . WPFNL_VERSION);
        delete_transient('wpfunnels_remote_template_data_52_' . WPFNL_VERSION);
        delete_transient('wpfunnels_remote_template_data_53_' . WPFNL_VERSION);

        do_action('wpfunnels/after_settings_saved', $payload );
        return [
            'success' => true
        ];
    }

	/**
	 * Persist the usage-tracking opt-in from the Advanced panel.
	 *
	 * The toggle only sends a value when the user actually changed it (see
	 * getTrackingConsentChange() in js/settings.js), so an unrelated save on a
	 * site that has never answered the setup-wizard question leaves that
	 * "undecided" state alone instead of writing an explicit "no". Consent
	 * itself lives in the Linno telemetry option, not in general settings, and
	 * the SDK migrates the legacy `wpfunnels_allow_tracking` key on read — so
	 * existing users keep whatever they agreed to during onboarding.
	 *
	 * @param array $payload Settings payload from the save request.
	 *
	 * @since 3.13.0
	 */
	private function save_tracking_consent( $payload ) {
		if ( ! isset( $payload['allow_tracking'] ) ) {
			return;
		}

		$submitted = sanitize_text_field( $payload['allow_tracking'] );

		if ( ! in_array( $submitted, [ 'yes', 'no' ], true ) ) {
			return;
		}

		$consent = 'yes' === $submitted;

		$telemetry = \WPFunnels\Tracking\Telemetry::instance();

		if ( ! $telemetry ) {
			return;
		}

		// Nothing to do when the toggle already matches what is stored — this
		// also stops a redundant save from turning "never decided" into "no".
		if ( $telemetry->get_consent_state() === ( $consent ? 'yes' : 'no' ) ) {
			return;
		}

		$telemetry->save_consent( $consent );

		do_action( 'wpfunnels_tracking_consent_updated', $consent );
	}


	/**
	 * Save Facebook pixel settings
	 *
	 * @param $settings
	 *
	 * @since 1.0.0
	 */
	public function save_recaptcha_settings( $settings ) {
		$enable_recapcha        = isset($settings['enable_recaptcha']) ? $settings['enable_recaptcha'] : 'no';
		$recapcha_site_key 		= isset($settings['recaptcha_site_key']) ? $settings['recaptcha_site_key'] : '';
		$recapcha_site_secret 	= isset($settings['recaptcha_site_secret']) ? $settings['recaptcha_site_secret'] : '';
		$recaptcha_settings 	= array(
			'enable_recaptcha'  => $enable_recapcha,
			'recaptcha_site_key'  => $recapcha_site_key,
			'recaptcha_site_secret' => $recapcha_site_secret
		);
		Wpfnl_functions::update_admin_settings('_wpfunnels_recaptcha_setting', $recaptcha_settings );
	}


	/**
	 * Save user role management data
	 *
	 * @param $payload
	 *
	 * @since 3.1.4
	 */
	public function save_user_role_management_data($payload) {
		if ( !isset($payload['user_role_management']) ) {
			return;
		}

		$roles 				= wp_unslash( $payload['user_role_management']['roles'] );
		$sanitized_settings = array();
		foreach ( $roles as $key => $value ) {
			$sanitized_settings[ $key ] = ( isset( $roles[ $key ] ) ) ? sanitize_text_field( $value ) : '';
		}
		Wpfnl_functions::update_admin_settings('_wpfunnels_user_roles', $sanitized_settings );

		$this->update_user_role_management( $sanitized_settings );
	}


	/**
	 * Update user role management data
	 *
	 * @param $sanitized_settings
	 *
	 * @since 3.1.4
	 */
	private function update_user_role_management( $sanitized_settings ) {

		foreach ( $sanitized_settings as $user_role => $value ) {
			$user_role_obj = get_role( $user_role );
			if ( $user_role_obj ) {
				if ( 'yes' === $value ) {
					$user_role_obj->add_cap('wpf_manage_funnels' );
				} elseif ( 'no' === $value ) {
					$user_role_obj->remove_cap('wpf_manage_funnels' );
				}
			}
		}
	}



	/**
	 * Save google map api key settings key
	 *
	 *  @param $payload
	 *
	 * @since 3.1.3
	 */
	public function save_google_map_key( $payload ) {
		if ( isset($payload['google_map_api_key']) ) {
			Wpfnl_functions::update_admin_settings('_wpfunnels_google_map_api_key', $payload['google_map_api_key'] );
		}
	}


	/**
	 * Save LMS provider settings
	 *
	 * @param $payload
	 *
	 * @since 2.0.0
	 */
	public function save_lms_settings( $payload ) {
		if ( isset($payload['lms_provider']) ) {
			$lms_settings = array(
				'lms_provider' => sanitize_text_field( $payload['lms_provider'] )
			);
			update_option( '_wpfunnels_lms_settings', $lms_settings );
		}
	}
	
	/**
	 * Save notification settings
	 *
	 * @param $payload
	 *
	 * @since 3.2.0
	 */
	public function save_notification_settings( $payload ) {
		$old_settings = Wpfnl_functions::get_notification_settings();
		$old_enabled = isset($old_settings['enable_revenue_report']) ? $old_settings['enable_revenue_report'] : 'no';
		
		$notification_settings = array(
			'enable_revenue_report' => isset($payload['enable_revenue_report']) ? sanitize_text_field($payload['enable_revenue_report']) : 'no',
			'revenue_report_frequency' => isset($payload['revenue_report_frequency']) ? sanitize_text_field($payload['revenue_report_frequency']) : 'weekly',
			'revenue_report_recipient' => isset($payload['revenue_report_recipient']) ? sanitize_text_field($payload['revenue_report_recipient']) : get_option('admin_email'),
			'revenue_report_subject' => isset($payload['revenue_report_subject']) ? sanitize_text_field($payload['revenue_report_subject']) : 'Store Revenue Report - {period}',
			'send_time' => isset($payload['send_time']) ? sanitize_text_field($payload['send_time']) : '10:00',
		);
		
		Wpfnl_functions::update_admin_settings('_wpfunnels_notification_settings', $notification_settings );
		
		$new_enabled = $notification_settings['enable_revenue_report'];
		
		// Handle action scheduler based on enable/disable
		if ( $new_enabled === 'yes' && $old_enabled !== 'yes' ) {
			// Enabled: Schedule the action
			$this->schedule_revenue_report_email( $notification_settings );
		} elseif ( $new_enabled !== 'yes' && $old_enabled === 'yes' ) {
			// Disabled: Remove the scheduled action
			$this->unschedule_revenue_report_email();
		} elseif ( $new_enabled === 'yes' ) {
			// Already enabled but settings changed: Reschedule
			$this->unschedule_revenue_report_email();
			$this->schedule_revenue_report_email( $notification_settings );
		}
	}


	/**
	 * Schedule revenue report email using Action Scheduler
	 *
	 * @param array $settings
	 *
	 * @since 3.2.0
	 */
	private function schedule_revenue_report_email( $settings ) {
		Wpfnl_functions::schedule_revenue_report_email( $settings );
	}


	/**
	 * Unschedule revenue report email
	 *
	 * @since 3.2.0
	 */
	private function unschedule_revenue_report_email() {
		Wpfnl_functions::unschedule_revenue_report_email();
	}


	/**
	 * Persist the AI Assistant fields that ride along with the settings save.
	 *
	 * Keys are stored through AISettings::connect(), which encrypts them. A
	 * blank field means "keep the stored key", so an unrelated settings save
	 * never wipes a connection.
	 *
	 * @param array $payload Settings payload.
	 *
	 * @since 3.13.0
	 * @return void
	 */
	private function save_ai_settings( $payload ) {
		if ( ! class_exists( '\WPFunnels\AI\Settings\AISettings' ) ) {
			return;
		}

		$settings = '\WPFunnels\AI\Settings\AISettings';

		foreach ( array( 'anthropic', 'openai', 'gemini' ) as $provider ) {
			$field = 'ai_' . $provider . '_key';
			$key   = isset( $payload[ $field ] ) ? trim( sanitize_text_field( $payload[ $field ] ) ) : '';

			// An untouched field arrives blank, and the placeholder mask must
			// never be stored as a credential.
			if ( '' === $key || false !== strpos( $key, '•' ) ) {
				continue;
			}

			call_user_func( array( $settings, 'connect' ), $provider, $key );
		}

		if ( isset( $payload['ai_provider'] ) && '' !== $payload['ai_provider'] ) {
			$provider = sanitize_text_field( $payload['ai_provider'] );

			// wordpress_ai needs no key; activating it is the whole connection.
			if ( 'wordpress_ai' === $provider && ! call_user_func( array( $settings, 'isConnected' ), $provider ) ) {
				call_user_func( array( $settings, 'connect' ), $provider, '' );
			}

			call_user_func( array( $settings, 'setActiveProvider' ), $provider );
		}

		if ( isset( $payload['ai_custom_instructions'] ) ) {
			call_user_func( array( $settings, 'saveCustomInstructions' ), sanitize_textarea_field( $payload['ai_custom_instructions'] ) );
		}

		if ( isset( $payload['ai_enabled'] ) ) {
			call_user_func( array( $settings, 'setEnabled' ), 'yes' === $payload['ai_enabled'] );
		}

		if ( isset( $payload['mcp_enabled'] ) ) {
			update_option( '_wpfnl_mcp_enabled', 'yes' === $payload['mcp_enabled'] ? 'yes' : 'no' );
		}
	}


    /**
     * Clear saved templates data
     *
     * @param $payload
     *
     * @return array
     * @since  1.0.0
     */
    public function clear_templates_data($payload) {

        delete_option(WPFNL_TEMPLATES_OPTION_KEY.'_wc');
        delete_option(WPFNL_TEMPLATES_OPTION_KEY.'_lms');

		$builder = Wpfnl_functions::get_builder_type();
		
        delete_transient('wpfunnels_remote_template_data_wc_' . WPFNL_VERSION);
        delete_transient('wpfunnels_remote_template_data_lms_' . WPFNL_VERSION);
        delete_transient('wpfunnels_remote_template_data_lead_' . WPFNL_VERSION);
        delete_transient('wpfunnels_remote_template_data_store_checkout_' . WPFNL_VERSION);

		delete_transient('wpfunnels_remote_template_data_wc_'. $builder . '_' . WPFNL_VERSION);
        delete_transient('wpfunnels_remote_template_data_lms_'. $builder . '_' . WPFNL_VERSION);
        delete_transient('wpfunnels_remote_template_data_lead_'. $builder . '_' . WPFNL_VERSION);
        delete_transient('wpfunnels_remote_template_data_store_checkout_'. $builder . '_' . WPFNL_VERSION);

        return [
            'success' => true
        ];
    }


	/**
     * Clear transient
     *
	 * @param $payload
     *
	 * @return array
	 */
    public function clear_transient_cache_data($payload) {

		delete_transient('wpfunnels_remote_template_data_' . WPFNL_VERSION);
        delete_transient('wpfunnels_remote_template_data_wc_' . WPFNL_VERSION);
        delete_transient('wpfunnels_remote_template_data_lms_' . WPFNL_VERSION);
        delete_transient('wpfunnels_remote_template_data_lead_' . WPFNL_VERSION);
        delete_transient('wpfunnels_remote_template_data_store_checkout_' . WPFNL_VERSION);
		delete_transient('wpfunnels_rollback_versions_' . WPFNL_VERSION);
		do_action('wpfunnels/after_clear_transient');
		return array(
			'success' => true
		);
	}


    /**
     * After settings saved hooks
     *
     * @since 1.0.0
     */
    public function after_permalink_settings_saved()
    {

        if( Wpfnl_functions::maybe_funnel_page() ){
            $is_permalink_saved = get_option('_wpfunnels_permalink_saved');
            if ($is_permalink_saved) {
                flush_rewrite_rules();
                delete_option('_wpfunnels_permalink_saved');
            }
        }
    }


    public function get_name()
    {
        return __('settings','wpfnl');
    }


	/**
	 * Get rollback version of WPF
	 *
	 * @return array|mixed
	 * @since  2.3.0
	 */
    public function get_roll_back_versions() {
		$rollback_versions = get_transient( 'wpfunnels_rollback_versions_' . WPFNL_VERSION );
		if ( false === $rollback_versions ) {
			$max_versions = 10;
			require_once ABSPATH . 'wp-admin/includes/plugin-install.php';
			$plugin_information = plugins_api(
				'plugin_information', [
					'slug' => 'wpfunnels',
				]
			);
			if ( empty( $plugin_information->versions ) || ! is_array( $plugin_information->versions ) ) {
				return [];
			}

			natsort( $plugin_information->versions );
			$plugin_information->versions = array_reverse($plugin_information->versions);
			$rollback_versions = [];

			$current_index = 0;
			foreach ( $plugin_information->versions as $version => $download_link ) {
				if ( $max_versions <= $current_index ) {
					break;
				}

				$lowercase_version = strtolower( $version );
				$is_valid_rollback_version = ! preg_match( '/(trunk|beta|rc|dev)/i', $lowercase_version );

				/**
				 * Is rollback version is valid.
				 *
				 * Filters the check whether the rollback version is valid.
				 *
				 * @param bool $is_valid_rollback_version Whether the rollback version is valid.
				 */
				$is_valid_rollback_version = apply_filters(
					'wpfunnels/is_valid_rollback_version',
					$is_valid_rollback_version,
					$lowercase_version
				);

				if ( ! $is_valid_rollback_version ) {
					continue;
				}

				if ( version_compare( $version, WPFNL_VERSION, '>=' ) ) {
					continue;
				}

				$current_index++;
				$rollback_versions[] = $version;
			}

			set_transient( 'wpfunnels_rollback_versions_' . WPFNL_VERSION, $rollback_versions, DAY_IN_SECONDS );
		}

		return $rollback_versions;
	}


	public function post_wpfunnels_rollback() {
		check_admin_referer( 'wpfunnels_rollback' );

		$rollback_versions = $this->get_roll_back_versions();
		if ( empty( $_GET['version'] ) || ! in_array( $_GET['version'], $rollback_versions ) ) {
			wp_die( esc_html__( 'Error occurred, The version selected is invalid. Try selecting different version.', 'wpfnl' ) );
		}

		$plugin_slug = basename( 'wpfunnels', '.php' );

		$rollback = new Rollback(
			[
				'version' => $_GET['version'],
				'plugin_name' => WPFNL_BASE,
				'plugin_slug' => $plugin_slug,
				'package_url' => sprintf( 'https://downloads.wordpress.org/plugin/%1s.%2s.zip', $plugin_slug, $_GET['version'] ),
			]
		);

		$rollback->run();

		wp_die(
			'', esc_html__( 'Rollback to Previous Version', 'wpfnl' ), [
				'response' => 200,
			]
		);
	}


    /**
     * WPFunnels log
     *
     * @param $payload
     *
     * @return array
     */
    public static function wpfnl_show_log( $payload ) {
        if( isset($payload['logKey']) ){
            // Reduce to a bare filename — strips any directory traversal (../),
            // separators, and stream-wrapper prefixes (phar://, php://, etc.).
            $key = basename( wp_unslash( $payload['logKey'] ) );

            $upload_dir = wp_upload_dir( null, false );
            $log_url    = trailingslashit( $upload_dir['basedir'] ) . 'wpfunnels/wpfunnels-logs/';
            $file_url   = $log_url . $key;

            // Whitelist: key must be a .log filename that actually resolves inside
            // the logs directory. realpath() comparison defeats any residual traversal.
            $real_dir  = realpath( $log_url );
            $real_file = file_exists( $file_url ) ? realpath( $file_url ) : false;

            $is_valid = $key
                && '.log' === strtolower( substr( $key, -4 ) )
                && $real_dir
                && $real_file
                && 0 === strpos( $real_file, trailingslashit( $real_dir ) );

            if ( $is_valid ) {
                // Never include/execute the log file — read it as data and escape it.
                $out = file_get_contents( $file_url );
                $out = '<pre>' . esc_html( $out ) . '</pre>';

                return array(
                    'success'  => true,
                    'content'  => $out,
                    'file_url' => $file_url,
                );
            }

            return array(
                'success'  => false,
                'content'  => '',
                'file_url' => '',
            );
        }

        return array(
            'success' => false,
            'content' => '',
            'file_url' => ''
        );


    }


	/**
	 * Send test notification email
	 *
	 * @param $payload
	 *
	 * @return array
	 * @since 3.2.0
	 */
	public function send_test_notification( $payload ) {
		if ( ! isset( $payload['email'] ) || ! is_email( $payload['email'] ) ) {
			return array(
				'success' => false,
				'message' => __( 'Please provide a valid email address.', 'wpfnl' )
			);
		}

		$settings = Wpfnl_functions::get_notification_settings();
		$subject_template = isset( $settings['revenue_report_subject'] ) ? $settings['revenue_report_subject'] : 'Store Revenue Report - {period}';
		
		// Calculate date range for test email (last week Monday to Sunday)
		$start_date = date( 'Y-m-d', strtotime( 'last monday -1 week' ) );
		$end_date = date( 'Y-m-d', strtotime( 'last sunday' ) );
		$date_range = date( 'F j, Y', strtotime( $start_date ) ) . ' - ' . date( 'F j, Y', strtotime( $end_date ) );
		
		$subject = '[Test] ' . str_replace( '{period}', $date_range, $subject_template );
		
		$to = sanitize_email( $payload['email'] );
		$message = $this->get_revenue_report_email_content( 'weekly' );
		$headers = array( 'Content-Type: text/html; charset=UTF-8' );

		$sent = wp_mail( $to, $subject, $message, $headers );

		if ( $sent ) {
			return array(
				'success' => true,
				'message' => __( 'Test email sent successfully! Please check your inbox.', 'wpfnl' )
			);
		} else {
			return array(
				'success' => false,
				'message' => __( 'Failed to send test email. Please check your email configuration.', 'wpfnl' )
			);
		}
	}


	/**
	 * Send revenue report email (triggered by Action Scheduler)
	 *
	 * @since 3.2.0
	 */
	public function send_revenue_report_email() {
		$settings = Wpfnl_functions::get_notification_settings();
		
		// Check if still enabled
		if ( ! isset( $settings['enable_revenue_report'] ) || $settings['enable_revenue_report'] !== 'yes' ) {
			return;
		}
		
		$recipients = isset( $settings['revenue_report_recipient'] ) ? $settings['revenue_report_recipient'] : get_option('admin_email');
		$subject = isset( $settings['revenue_report_subject'] ) ? $settings['revenue_report_subject'] : 'Store Revenue Report - {period}';
		$frequency = isset( $settings['revenue_report_frequency'] ) ? $settings['revenue_report_frequency'] : 'weekly';
		
		// Calculate date range based on frequency
		if ( $frequency === 'monthly' ) {
			$start_date = date( 'Y-m-d', strtotime( 'first day of last month' ) );
			$end_date = date( 'Y-m-d', strtotime( 'last day of last month' ) );
		} else {
			// Weekly: Last week Monday to Sunday
			$start_date = date( 'Y-m-d', strtotime( 'last monday -1 week' ) );
			$end_date = date( 'Y-m-d', strtotime( 'last sunday' ) );
		}
		
		$date_range = date( 'F j, Y', strtotime( $start_date ) ) . ' - ' . date( 'F j, Y', strtotime( $end_date ) );
		
		// Replace {period} placeholder with date range
		$subject = str_replace( '{period}', $date_range, $subject );
		
		// Generate report content
		$message = $this->get_revenue_report_email_content( $frequency );
		
		// Prepare headers
		$headers = array( 'Content-Type: text/html; charset=UTF-8' );
		
		// Send email to recipients (can be multiple, comma-separated)
		$recipient_array = array_map( 'trim', explode( ',', $recipients ) );
		
		foreach ( $recipient_array as $recipient ) {
			if ( is_email( $recipient ) ) {
				wp_mail( sanitize_email( $recipient ), $subject, $message, $headers );
			}
		}
	}


	/**
	 * Get revenue report email content with actual data
	 *
	 * @param string $frequency
	 * @return string
	 * @since 3.2.0
	 */
	private function get_revenue_report_email_content( $frequency = 'weekly' ) {
		$site_name = get_bloginfo( 'name' );
		$site_url = home_url();
		
		// Calculate date range
		if ( $frequency === 'monthly' ) {
			$start_date = date( 'Y-m-d', strtotime( 'first day of last month' ) );
			$end_date = date( 'Y-m-d', strtotime( 'last day of last month' ) );
			$period_text = __( 'Monthly', 'wpfnl' );
		} else {
			// Weekly: Last week Monday to Sunday
			$start_date = date( 'Y-m-d', strtotime( 'last monday -1 week' ) );
			$end_date = date( 'Y-m-d', strtotime( 'last sunday' ) );
			$period_text = __( 'Weekly', 'wpfnl' );
		}
		
		$date_range = date( 'F j, Y', strtotime( $start_date ) ) . ' - ' . date( 'F j, Y', strtotime( $end_date ) );
		
		// Calculate previous period for comparison
		if ( $frequency === 'monthly' ) {
			$prev_start_date = date( 'Y-m-d', strtotime( 'first day of -2 months' ) );
			$prev_end_date = date( 'Y-m-d', strtotime( 'last day of -2 months' ) );
		} else {
			$prev_start_date = date( 'Y-m-d', strtotime( 'last monday -2 weeks' ) );
			$prev_end_date = date( 'Y-m-d', strtotime( 'last sunday -1 week' ) );
		}
		
		// Get revenue data for current and previous periods
		$revenue_data = $this->get_revenue_data( $start_date, $end_date );
		$previous_data = $this->get_revenue_data( $prev_start_date, $prev_end_date );
		
		// Calculate percentage changes
		$revenue_data['total_orders_change'] = $this->calculate_percentage_change( $previous_data['total_orders'], $revenue_data['total_orders'] );
		$revenue_data['total_customers_change'] = $this->calculate_percentage_change( $previous_data['total_customers'], $revenue_data['total_customers'] );
		$revenue_data['total_revenue_change'] = $this->calculate_percentage_change( $previous_data['total_revenue_raw'], $revenue_data['total_revenue_raw'] );
		$revenue_data['average_order_value_change'] = $this->calculate_percentage_change( $previous_data['average_order_value_raw'], $revenue_data['average_order_value_raw'] );
		
		// Build email with FunnelKit-style design
		$message = '<!DOCTYPE html>';
		$message .= '<html lang="en">';
		$message .= '<head>';
		$message .= '<meta charset="UTF-8">';
		$message .= '<meta name="viewport" content="width=device-width, initial-scale=1.0">';
		$message .= '<title>Performance Report</title>';
		$message .= '</head>';
		$message .= '<body style="margin: 0; padding: 0; font-family: -apple-system, BlinkMacSystemFont, \'Segoe UI\', Roboto, \'Helvetica Neue\', Arial, sans-serif; background-color: #f5f5f5;">';
		
		// Main container
		$message .= '<table width="100%" border="0" cellspacing="0" cellpadding="0" style="background-color: #f5f5f5; padding: 40px 20px;">';
		$message .= '<tr><td align="center">';
		$message .= '<table width="600" border="0" cellspacing="0" cellpadding="0" style="background-color: #ffffff; border-radius: 8px; overflow: hidden; box-shadow: 0 2px 8px rgba(0,0,0,0.1);">';
		
		// Top orange banner with text
		$message .= '<tr><td style="background-color: #6e42d3; padding: 12px 40px; text-align: center;">';
		$message .= '<div style="font-size: 16px; font-weight: 600; color: #ffffff;">Your ' . esc_html( strtolower( $frequency ) ) . ' summary from WPFunnels.</div>';
		$message .= '</td></tr>';
		
		// WPFunnels logo section
		$message .= '<tr><td style="background-color: #ffffff; padding: 20px 40px 20px; text-align: center;">';
		$message .= '<svg width="38" height="28" viewBox="0 0 38 28" fill="none" xmlns="http://www.w3.org/2000/svg" style="display: inline-block; margin-right: 10px; vertical-align: middle;">';
		$message .= '<path d="M7.01532 18H31.9847L34 11H5L7.01532 18Z" fill="#EE8134"/>';
		$message .= '<path d="M11.9621 27.2975C12.0923 27.7154 12.4792 28 12.9169 28H26.0831C26.5208 28 26.9077 27.7154 27.0379 27.2975L29 21H10L11.9621 27.2975Z" fill="#6E42D3"/>';
		$message .= '<path d="M37.8161 0.65986C37.61 0.247888 37.2609 0 36.8867 0H1.11326C0.739128 0 0.390003 0.247888 0.183972 0.65986C-0.0220592 1.07193 -0.0573873 1.59277 0.0898627 2.04655L1.69781 7H36.3022L37.9102 2.04655C38.0574 1.59287 38.022 1.07193 37.8161 0.65986Z" fill="#6E42D3"/>';
		$message .= '</svg>';
		$message .= '<span style="font-size: 32px; font-weight: bold; color: #231748; vertical-align: middle;">WPFunnels</span>';
		$message .= '</td></tr>';
		
		// Date range, Performance Report title, and View Button - all in same background
		$message .= '<tr><td style="padding: 20px 40px 30px; text-align: center; background-color: #f8f9fa;">';
		// Date range
		$message .= '<div style="font-size: 14px; color: #6c757d; font-weight: 500; margin-bottom: 20px;">' . esc_html( $date_range ) . '</div>';
		// Performance Report title
		$message .= '<h1 style="margin: 0 0 10px; font-size: 32px; font-weight: bold; color: #212529;">Store Revenue Summary</h1>';
		$message .= '<p style="margin: 0 0 20px; font-size: 14px; color: #6c757d;">Track your store performance and revenue metrics for this period</p>';
		// View Dashboard Button
		$message .= '<a href="' . esc_url( admin_url( 'admin.php?page=wpfunnels' ) ) . '" style="display: inline-block; background-color: #6e42d3; color: #ffffff; text-decoration: none; padding: 12px 30px; border-radius: 5px; font-size: 14px; font-weight: 600;">View Dashboard</a>';
		$message .= '</td></tr>';
		
		// Key Performance Metrics section
		$message .= '<tr><td style="padding: 20px 40px;">';
		$message .= '<h2 style="margin: 0 0 10px; font-size: 24px; font-weight: bold; color: #212529; text-align: center;">Revenue Overview</h2>';
		$message .= '<p style="margin: 0 0 30px; font-size: 14px; color: #6c757d; text-align: center;">Your ' . esc_html( strtolower( $frequency ) ) . ' performance snapshot</p>';
		
		// Metrics grid
		$message .= '<table width="100%" border="0" cellspacing="0" cellpadding="0">';
		
		// Row 1: Total orders and Total contacts
		$message .= '<tr>';
		$message .= '<td width="50%" style="padding: 20px; vertical-align: top;">';
		$message .= '<div style="text-align: left;">';
		$message .= '<div style="font-size: 14px; color: #6c757d; margin-bottom: 10px; font-weight: 500;">Total orders</div>';
		$message .= '<div style="font-size: 36px; font-weight: bold; color: #212529; margin-bottom: 5px;">' . esc_html( $revenue_data['total_orders'] ) . '</div>';
		$message .= $revenue_data['total_orders_change'];
		$message .= '</div>';
		$message .= '</td>';
		$message .= '<td width="50%" style="padding: 20px; vertical-align: top;">';
		$message .= '<div style="text-align: left;">';
		$message .= '<div style="font-size: 14px; color: #6c757d; margin-bottom: 10px; font-weight: 500;">Total customers</div>';
		$message .= '<div style="font-size: 36px; font-weight: bold; color: #212529; margin-bottom: 5px;">' . esc_html( $revenue_data['total_customers'] ) . '</div>';
		$message .= $revenue_data['total_customers_change'];
		$message .= '</div>';
		$message .= '</td>';
		$message .= '</tr>';
		
		// Row 2: Revenue and Bump revenue
		$message .= '<tr>';
		$message .= '<td width="50%" style="padding: 20px; vertical-align: top;">';
		$message .= '<div style="text-align: left;">';
		$message .= '<div style="font-size: 14px; color: #6c757d; margin-bottom: 10px; font-weight: 500;">Revenue</div>';
		$message .= '<div style="font-size: 36px; font-weight: bold; color: #212529; margin-bottom: 5px;">' . wp_strip_all_tags( $revenue_data['total_revenue'] ) . '</div>';
		$message .= $revenue_data['total_revenue_change'];
		$message .= '</div>';
		$message .= '</td>';
		$message .= '<td width="50%" style="padding: 20px; vertical-align: top;">';
		$message .= '<div style="text-align: left;">';
		$message .= '<div style="font-size: 14px; color: #6c757d; margin-bottom: 10px; font-weight: 500;">Bump revenue</div>';
		if ( Wpfnl_functions::is_wpfnl_pro_activated() ) {
			$message .= '<div style="font-size: 36px; font-weight: bold; color: #212529; margin-bottom: 5px;">0 <span style="font-size: 18px; font-weight: normal; color: #6c757d;">USD</span></div>';
			$message .= '<div style="font-size: 12px; color: #6c757d;">0% - Previous period</div>';
		} else {
			$message .= '<div style="font-size: 36px; margin-bottom: 5px;"><svg width="32" height="32" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M17 11V7C17 4.79086 15.2091 3 13 3H11C8.79086 3 7 4.79086 7 7V11M12 14V16M8 11H16C17.1046 11 18 11.8954 18 13V19C18 20.1046 17.1046 21 16 21H8C6.89543 21 6 20.1046 6 19V13C6 11.8954 6.89543 11 8 11Z" stroke="#6c757d" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg></div>';
			$message .= '<div style="font-size: 12px; color: #6c757d;">WPFunnels Pro</div>';
		}
		$message .= '</div>';
		$message .= '</td>';
		$message .= '</tr>';
		
		// Row 3: Upsell revenue and Average order value
		$message .= '<tr>';
		$message .= '<td width="50%" style="padding: 20px; vertical-align: top;">';
		$message .= '<div style="text-align: left;">';
		$message .= '<div style="font-size: 14px; color: #6c757d; margin-bottom: 10px; font-weight: 500;">Upsell revenue</div>';
		if ( Wpfnl_functions::is_wpfnl_pro_activated() ) {
			$message .= '<div style="font-size: 36px; font-weight: bold; color: #212529; margin-bottom: 5px;">0 <span style="font-size: 18px; font-weight: normal; color: #6c757d;">USD</span></div>';
			$message .= '<div style="font-size: 12px; color: #6c757d;">0% - Previous period</div>';
		} else {
			$message .= '<div style="font-size: 36px; margin-bottom: 5px;"><svg width="32" height="32" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M17 11V7C17 4.79086 15.2091 3 13 3H11C8.79086 3 7 4.79086 7 7V11M12 14V16M8 11H16C17.1046 11 18 11.8954 18 13V19C18 20.1046 17.1046 21 16 21H8C6.89543 21 6 20.1046 6 19V13C6 11.8954 6.89543 11 8 11Z" stroke="#6c757d" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg></div>';
			$message .= '<div style="font-size: 12px; color: #6c757d;">WPFunnels Pro</div>';
		}
		$message .= '</div>';
		$message .= '</td>';
		$message .= '<td width="50%" style="padding: 20px; vertical-align: top;">';
		$message .= '<div style="text-align: left;">';
		$message .= '<div style="font-size: 14px; color: #6c757d; margin-bottom: 10px; font-weight: 500;">Average order value</div>';
		$message .= '<div style="font-size: 36px; font-weight: bold; color: #212529; margin-bottom: 5px;">' . wp_strip_all_tags( $revenue_data['average_order_value'] ) . '</div>';
		$message .= $revenue_data['average_order_value_change'];
		$message .= '</div>';
		$message .= '</td>';
		$message .= '</tr>';
		
		$message .= '</table>';
		$message .= '</td></tr>';
		
		// Additional revenue info
		if ( isset( $revenue_data['total_revenue_raw'] ) && $revenue_data['total_revenue_raw'] > 0 ) {
			$message .= '<tr><td style="padding: 20px 40px;">';
			$message .= '<div style="background-color: #fff3cd; border-left: 4px solid #ffc107; padding: 15px; border-radius: 4px;">';
			$message .= '<p style="margin: 0; font-size: 14px; color: #856404;">Since installing WPFunnels you have captured additional revenue of <strong>' . wp_kses_post( wc_price( $revenue_data['total_revenue_raw'] ) ) . '</strong></p>';
			$message .= '</div>';
			$message .= '</td></tr>';
		}
		
		// Footer
		$message .= '<tr><td style="padding: 30px 40px; text-align: center; background-color: #ffffff; border-top: 1px solid #e9ecef;">';
		$message .= '<p style="margin: 0 0 10px; font-size: 12px; color: #6c757d;">This performance report email was sent from the <a href="' . esc_url( $site_url ) . '" style="color: #007bff; text-decoration: none;">' . esc_html( $site_name ) . '</a> by admin ' . esc_html( date( 'F j, Y' ) ) . '</p>';
		$message .= '<p style="margin: 0; font-size: 12px; color: #6c757d;"><a href="' . esc_url( admin_url( 'admin.php?page=wpfnl_settings#notifications-settings' ) ) . '" style="color: #007bff; text-decoration: none;">Click here</a> to change the frequency and recipients.</p>';
		$message .= '</td></tr>';
		
		$message .= '</table>';
		$message .= '</td></tr>';
		$message .= '</table>';
		
		$message .= '</body>';
		$message .= '</html>';

		return $message;
	}


	/**
	 * Get revenue data from WPFunnels stats
	 *
	 * @param string $start_date
	 * @param string $end_date
	 * @return array
	 * @since 3.2.0
	 */
	private function get_revenue_data( $start_date, $end_date ) {
		global $wpdb;
		$table = $wpdb->prefix . 'wpfnl_stats';
		
		// Get total orders count
		$total_orders = $wpdb->get_var( $wpdb->prepare(
			"SELECT COUNT(id) FROM $table WHERE date_created >= %s AND date_created <= %s AND status = 'completed'",
			$start_date,
			$end_date . ' 23:59:59'
		) );
		
		// Get total revenue (total_sales includes everything: checkout + order bump + upsell + downsell)
		$total_revenue_raw = $wpdb->get_var( $wpdb->prepare(
			"SELECT SUM(total_sales) FROM $table WHERE date_created >= %s AND date_created <= %s AND status = 'completed'",
			$start_date,
			$end_date . ' 23:59:59'
		) );
		
		// Get total customers (unique customer_id)
		$total_customers = $wpdb->get_var( $wpdb->prepare(
			"SELECT COUNT(DISTINCT customer_id) FROM $table WHERE date_created >= %s AND date_created <= %s AND status = 'completed'",
			$start_date,
			$end_date . ' 23:59:59'
		) );
		
		// Calculate average order value
		$total_orders_int = intval( $total_orders );
		$total_revenue_float = floatval( $total_revenue_raw );
		$average = $total_orders_int > 0 ? $total_revenue_float / $total_orders_int : 0;
		
		// Format data
		$data = array(
			'total_revenue' => function_exists( 'wc_price' ) ? wc_price( $total_revenue_float ) : '$' . number_format( $total_revenue_float, 2 ),
			'total_revenue_raw' => $total_revenue_float,
			'total_orders' => $total_orders_int,
			'total_customers' => intval( $total_customers ),
			'average_order_value' => function_exists( 'wc_price' ) ? wc_price( $average ) : '$' . number_format( $average, 2 ),
			'average_order_value_raw' => $average,
		);
		
		return $data;
	}


	/**
	 * Calculate percentage change between two values
	 *
	 * @param float $old_value
	 * @param float $new_value
	 * @return string HTML formatted percentage change
	 * @since 3.2.0
	 */
	private function calculate_percentage_change( $old_value, $new_value ) {
		$old_value = floatval( $old_value );
		$new_value = floatval( $new_value );
		
		if ( $old_value == 0 ) {
			if ( $new_value > 0 ) {
				return '<div style="font-size: 12px; color: #28a745;">+100% - Previous period</div>';
			} else {
				return '<div style="font-size: 12px; color: #6c757d;">0% - Previous period</div>';
			}
		}
		
		$change = ( ( $new_value - $old_value ) / $old_value ) * 100;
		$change_formatted = number_format( abs( $change ), 1 );
		
		if ( $change > 0 ) {
			return '<div style="font-size: 12px; color: #28a745;">+' . $change_formatted . '% - Previous period</div>';
		} elseif ( $change < 0 ) {
			return '<div style="font-size: 12px; color: #dc3545;">-' . $change_formatted . '% - Previous period</div>';
		} else {
			return '<div style="font-size: 12px; color: #6c757d;">0% - Previous period</div>';
		}
	}


    /**
     * WPFunnels log
     *
     * @param $payload
     *
     * @return array
     */
    public static function wpfnl_delete_log( $payload ) {
        if( isset($payload['logKey']) ){
            $key        = sanitize_file_name( basename( $payload['logKey'] ) );
            $upload_dir = wp_upload_dir( null, false );
            $log_dir    = $upload_dir['basedir'].'/wpfunnels/wpfunnels-logs/';
            $file_name  = $log_dir . $key;

            // Verify the resolved path is within the log directory
            $real_file    = realpath($file_name);
            $real_log_dir = realpath($log_dir);
            
            if ( $real_file && $real_log_dir && strpos( $real_file, $real_log_dir ) === 0 ) {
                $response = \Wpfnl_Logger::delete_log_file( $file_name );
                if( $response ){
                    return array('success' => true);
                }
            }
        }

        return array(
            'success' => false,
            'content' => '',
            'file_url' => ''
        );
    }
}

```
