# woocommerce-pos/1.10.16/includes/Activator.php

WCPOS – Point of Sale (POS) plugin for WooCommerce, version 1.10.16. 718 lines.

- Page: https://pluginprobe.com/plugins/woocommerce-pos/1.10.16/code/includes/Activator.php
- Raw: https://pluginprobe.com/plugins/woocommerce-pos/1.10.16/raw/includes/Activator.php
- Modified: 2026-09-06T10:00:12+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/woocommerce-pos/1.10.16/code/includes/Activator.php#L10-L20`.

```php
<?php
/**
 * Activation checks and set up.
 *
 * @author    Paul Kilmurray <paul@kilbot.com>
 *
 * @see      http://wcpos.com
 * @package WCPOS\WooCommercePOS
 */

namespace WCPOS\WooCommercePOS;

use WCPOS\WooCommercePOS\Admin\Consent;
use WCPOS\WooCommercePOS\Services\Lifecycle_Events;
use WCPOS\WooCommercePOS\Sync\Api as Sync_Api;
use WCPOS\WooCommercePOS\Sync\Health as Sync_Health;
use WCPOS\WooCommercePOS\Sync\Integrity_Digest;
use WCPOS\WooCommercePOS\Sync\Mutation_Store;
use WCPOS\WooCommercePOS\Sync\Sync_Journal;
use const DOING_AJAX;

/**
 * Activator class.
 */
class Activator {
	/**
	 * Lock name used by WP_Upgrader::create_lock().
	 */
	private const DB_UPGRADE_LOCK_NAME = 'woocommerce_pos_db_upgrade_lock';

	/**
	 * Lock TTL in seconds.
	 */
	private const DB_UPGRADE_LOCK_TTL = 600;

	/**
	 * Constructor.
	 */
	public function __construct() {
		register_activation_hook( PLUGIN_FILE, array( $this, 'activate' ) );
		add_action( 'wpmu_new_blog', array( $this, 'activate_new_site' ) );
		add_action( 'plugins_loaded', array( $this, 'init' ) );
	}

	/**
	 * Checks for valid install and begins execution of the plugin.
	 */
	public function init(): void {
		// Check for min requirements to run.
		if ( $this->php_check() && $this->woocommerce_check() ) {
			// Defer permalink check to admin_init so __() calls happen after
			// after_setup_theme (WordPress 6.7+ triggers a notice otherwise).
			if ( is_admin() && ( ! \defined( '\DOING_AJAX' ) || ! DOING_AJAX ) ) { // @phpstan-ignore-line
				add_action(
					'admin_init',
					function () {
						$this->permalink_check();
					}
				);
			}

			// Init update script if required.
			$this->version_check();
			$this->pro_version_check();

			// resolve plugin plugins.
			$this->plugin_check();

			new Init();
		}
	}

	/**
	 * Fired when the plugin is activated.
	 *
	 * @param bool $network_wide Whether to activate network-wide.
	 */
	public function activate( $network_wide ): void {
		if ( \function_exists( 'is_multisite' ) && is_multisite() ) {
			if ( $network_wide ) {
				// Get all blog ids.
				$blog_ids = $this->get_blog_ids();

				foreach ( $blog_ids as $blog_id ) {
					switch_to_blog( $blog_id );
					$this->single_activate();

					restore_current_blog();
				}
			} else {
				self::single_activate();
			}
		} else {
			self::single_activate();
		}
	}

	/**
	 * Fired when the plugin is activated.
	 *
	 * @param bool $install_sync_schema Whether to install the sync schema.
	 */
	public function single_activate( bool $install_sync_schema = true ): void {
		$role_capabilities = self::role_capability_definition();

		// Reseed the default template terms on the next request: (re)activation
		// is the repair a merchant reaches for after deleting a term by hand.
		// This also runs once per upgrade (version_check re-activates to sync
		// role caps), so one post-upgrade request pays the ~18 seeding queries.
		delete_option( Templates::DEFAULT_TERMS_OPTION );

		// Second, merchant-reachable trigger for the autoload repair: db_upgrade()
		// only runs when version_check() trips on an admin load that reaches
		// woocommerce_init, and a miss there is permanent once bump_versions() ran.
		self::autoload_request_latches();
		Admin\Permalink::ensure_default();

		// create POS specific roles.
		$this->create_pos_roles();

		// add pos capabilities to non POS roles.
		$this->add_pos_capability(
			array(
				'administrator' => $role_capabilities['administrator'],
				'shop_manager'  => $role_capabilities['shop_manager'],
			)
		);

		$stored_roles        = get_option( wp_roles()->role_key, array() );
		$roles_are_persisted = is_array( $stored_roles );
		if ( $roles_are_persisted ) {
			foreach ( $role_capabilities as $slug => $capabilities ) {
				$required_capabilities = 'cashier' === $slug
					? array_merge( array( 'access_woocommerce_pos' ), array_keys( $capabilities ) )
					: $capabilities;
				foreach ( $required_capabilities as $capability ) {
					if ( empty( $stored_roles[ $slug ]['capabilities'][ $capability ] ) ) {
						$roles_are_persisted = false;
						break 2;
					}
				}
			}
		}

		$obsolete_customer_create_cap = isset( $role_capabilities['cashier']['create_customers'] ) ? 'promote_users' : 'create_customers';
		if ( $roles_are_persisted && empty( $stored_roles['cashier']['capabilities'][ $obsolete_customer_create_cap ] ) ) {
			update_option( 'woocommerce_pos_role_caps_fingerprint', $this->role_caps_fingerprint(), true );
		}

		// Flag the consent pop-up for the next admin page load. Done here
		// because the `activated_plugin` action in Admin\Consent fires
		// inside the activation request, at which point our plugin's
		// `plugins_loaded` callback hasn't yet instantiated Init on a
		// fresh install.
		//
		// Read the option directly — woocommerce_pos_get_settings() lives in
		// wcpos-functions.php which Init loads on `plugins_loaded`, but
		// plugins_loaded has already fired by the time activation runs.
		$general_settings = get_option( 'woocommerce_pos_settings_general', array() );
		$tracking_consent = is_array( $general_settings ) && isset( $general_settings['tracking_consent'] )
			? $general_settings['tracking_consent']
			: 'undecided';
		if ( 'undecided' === $tracking_consent ) {
			set_transient( Consent::MODAL_TRANSIENT, 1, Consent::MODAL_TRANSIENT_TTL );
		}

		if ( $install_sync_schema ) {
			$this->install_sync_schema();
		}

		// Record the install for analytics. Consent is still `undecided` at this
		// point, so the event is held until the user answers the pop-up flagged
		// above; Lifecycle_Events owns that deferral and reports at most once.
		( new Lifecycle_Events() )->record_install();
	}

	/**
	 * Install the sync store and latch its aggregate schema version after verification.
	 */
	public function install_sync_schema(): void {
		$previous_schema = get_option( Sync_Api::SCHEMA_OPTION, null );

		$journal = new Sync_Journal();
		$journal->install();
		( new Integrity_Digest() )->install();
		( new Mutation_Store() )->install();

		if ( ! Sync_Health::is_healthy() ) {
			if ( Sync_Api::SCHEMA_VERSION === $previous_schema ) {
				delete_option( Sync_Api::SCHEMA_OPTION );
			}
			return;
		}

		// Schema 3 (#1379): the customer space widened from role=customer to ALL users.
		// Upgrading installs carry role-departure tombstones in the persisted stream that
		// would replay against now-live users; compensating updates supersede them (see
		// Sync_Journal::append_customer_updates_for_all_users). The old latch stays until
		// migration succeeds, so retries may append duplicate but harmless superseding
		// updates. Fresh installs (no previous latch) have no stream to repair.
		if (
			null !== $previous_schema
			&& version_compare( (string) $previous_schema, '3', '<' )
			&& ! $journal->append_customer_updates_for_all_users()
		) {
			return;
		}

		// Autoloaded: the Init constructor reads this latch on every request.
		// This flips an existing row only on WP 6.4+; older rows are flipped by
		// autoload_request_latches() on upgrade.
		update_option( Sync_Api::SCHEMA_OPTION, Sync_Api::SCHEMA_VERSION, true );

		if ( null !== $previous_schema && version_compare( (string) $previous_schema, Sync_Api::SCHEMA_VERSION, '<' ) ) {
			global $wpdb;
			$wpdb->query( "DROP TABLE IF EXISTS {$wpdb->prefix}wcpos_sync_change_log" ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Known legacy table name.
			$wpdb->query( "DROP TABLE IF EXISTS {$wpdb->prefix}wcpos_sync_order_index" ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Known legacy table name.
			// The legacy Change_Log_Purge class is gone; its recurring cron event
			// would otherwise survive the upgrade and fire a hook with no handler
			// forever. Literal hook name — the constant was removed with the class.
			wp_clear_scheduled_hook( 'wcpos_change_log_purge' );
		}
	}

	/**
	 * Fired when a new site is activated with a WPMU environment.
	 *
	 * @param int $blog_id Blog ID.
	 */
	public function activate_new_site( $blog_id ): void {
		if ( 1 !== did_action( 'wpmu_new_blog' ) ) {
			return;
		}

		switch_to_blog( $blog_id );
		$this->single_activate();
		restore_current_blog();
	}

	/**
	 * Check min version of PHP.
	 */
	private function php_check() {
		$php_version = PHP_VERSION;
		if ( version_compare( $php_version, PHP_MIN_VERSION, '>' ) ) {
			return true;
		}

		// Defer __() call to avoid "too early" warning in WordPress 6.7+.
		add_action(
			'admin_init',
			function () {
				$message = \sprintf(
					// translators: 1: Minimum PHP version, 2: Update URL.
					__( '<strong>WCPOS</strong> requires PHP %1$s or higher. Read more information about <a href="%2$s">how you can update</a>', 'woocommerce-pos' ),
					PHP_MIN_VERSION,
					'http://www.wpupdatephp.com/update/'
				) . ' &raquo;';

				Admin\Notices::add( $message );
			}
		);
	}

	/**
	 * Check min version of WooCommerce installed.
	 */
	private function woocommerce_check() {
		if ( class_exists( '\WooCommerce' ) && version_compare( WC()->version, WC_MIN_VERSION, '>=' ) ) {
			return true;
		}

		// Defer __() call to avoid "too early" warning in WordPress 6.7+.
		add_action(
			'admin_init',
			function () {
				$message = \sprintf(
					// translators: 1: WooCommerce URL, 2: Minimum WC version, 3: Plugins URL.
					__( '<strong>WCPOS</strong> requires <a href="%1$s">WooCommerce %2$s or higher</a>. Please <a href="%3$s">install and activate WooCommerce</a>', 'woocommerce-pos' ),
					'http://wordpress.org/plugins/woocommerce/',
					WC_MIN_VERSION,
					admin_url( 'plugins.php' )
				) . ' &raquo;';

				Admin\Notices::add( $message );
			}
		);
	}

	/**
	 * POS Frontend will give 404 if pretty permalinks not active.
	 */
	private function permalink_check(): void {
		$permalinks = get_option( 'permalink_structure' );

		// early return.
		if ( $permalinks ) {
			return;
		}

		$message = /* translators: Plugin activation notice label. */ __( '<strong>WooCommerce REST API</strong> requires <em>pretty</em> permalinks to work correctly', 'woocommerce-pos' ) . '. ';
		$message .= \sprintf( '<a href="%s">%s</a>', admin_url( 'options-permalink.php' ), /* translators: Plugin activation notice label. */ __( 'Enable permalinks', 'woocommerce-pos' ) ) . ' &raquo;';

		Admin\Notices::add( $message );
	}

	/**
	 * Check version number, runs every admin page load.
	 */
	private function version_check(): void {
		$old                  = (string) Services\Settings::get_db_version();
		$plugin_needs_upgrade = version_compare( $old, VERSION, '<' );
		$sync_needs_upgrade   = Sync_Api::SCHEMA_VERSION !== get_option( Sync_Api::SCHEMA_OPTION, null );

		$role_caps_fingerprint = $this->role_caps_fingerprint();
		$role_caps_need_sync   = get_option( 'woocommerce_pos_role_caps_fingerprint' ) !== $role_caps_fingerprint;
		if ( ! $plugin_needs_upgrade && ! $sync_needs_upgrade && ! $role_caps_need_sync ) {
			return;
		}

		if ( ! $this->acquire_db_upgrade_lock() ) {
			return;
		}

		$locked_old                  = (string) Services\Settings::get_db_version();
		$locked_plugin_needs_upgrade = version_compare( $locked_old, VERSION, '<' );
		$locked_sync_needs_upgrade   = Sync_Api::SCHEMA_VERSION !== get_option( Sync_Api::SCHEMA_OPTION, null );

		$locked_role_caps_fingerprint = $this->role_caps_fingerprint();
		$locked_role_caps_need_sync   = get_option( 'woocommerce_pos_role_caps_fingerprint' ) !== $locked_role_caps_fingerprint;
		if ( ! $locked_plugin_needs_upgrade && ! $locked_sync_needs_upgrade && ! $locked_role_caps_need_sync ) {
			$this->release_db_upgrade_lock();
			return;
		}

		if ( $locked_plugin_needs_upgrade ) {
			Services\Settings::bump_versions();
		}

		if ( $locked_plugin_needs_upgrade || $locked_role_caps_need_sync ) {
			// Re-run activation to sync role capabilities. add_role() and add_cap()
			// are both idempotent, so this is safe. Without this, capabilities added
			// in newer versions would never reach existing installs because add_role()
			// is a no-op when the role already exists.
			// Deferred to 'init' because create_pos_roles() calls __() which
			// requires translations to be loaded (WordPress 6.7+).
			add_action(
				'init',
				function () {
					$this->single_activate( false );
				}
			);
		}

		$lock_released = false;
		$release_lock  = function () use ( &$lock_released ): void {
			if ( $lock_released ) {
				return;
			}

			$lock_released = true;
			$this->release_db_upgrade_lock();
		};

		// Safety net in case woocommerce_init does not fire for this request.
		add_action( 'shutdown', $release_lock );

		// Defer db_upgrade to woocommerce_init when WC is fully loaded.
		// This prevents conflicts with plugins like WC Subscriptions that hook
		// into before_delete_post and assume WC()->order_factory is available.
		add_action(
			'woocommerce_init',
			function () use ( $locked_old, $locked_plugin_needs_upgrade, $locked_sync_needs_upgrade, $release_lock ) {
				try {
					$this->db_upgrade( $locked_old, VERSION );

					// Report the upgrade only once the migration has actually
					// completed — queueing it beside bump_versions() would claim a
					// finished upgrade even when db_upgrade() threw or never ran.
					// Still exactly-once: the version was bumped above, so the
					// upgrade is not re-detected on the next request.
					if ( $locked_plugin_needs_upgrade ) {
						( new Lifecycle_Events() )->record_upgrade( $locked_old, VERSION );
					}
					if ( $locked_sync_needs_upgrade && Sync_Api::SCHEMA_VERSION === get_option( Sync_Api::SCHEMA_OPTION, null ) ) {
						( new Sync_Journal() )->register_hooks();
						( new Integrity_Digest() )->register_hooks();
					}
				} finally {
					$release_lock();
					remove_action( 'shutdown', $release_lock );
				}
			}
		);
	}

	/**
	 * Acquire the DB upgrade lock.
	 *
	 * @return bool True when this request owns the lock.
	 */
	private function acquire_db_upgrade_lock(): bool {
		require_once ABSPATH . 'wp-admin/includes/class-wp-upgrader.php';

		return \WP_Upgrader::create_lock( self::DB_UPGRADE_LOCK_NAME, self::DB_UPGRADE_LOCK_TTL );
	}

	/**
	 * Release the DB upgrade lock.
	 */
	private function release_db_upgrade_lock(): void {
		if ( ! class_exists( '\WP_Upgrader', false ) ) {
			require_once ABSPATH . 'wp-admin/includes/class-wp-upgrader.php';
		}

		\WP_Upgrader::release_lock( self::DB_UPGRADE_LOCK_NAME );
	}

	/**
	 * Plugin conflicts.
	 *
	 * - NextGEN Gallery is a terrible plugin. It buffers all content on 'init' action, priority -1 and inserts junk code.
	 */
	private function plugin_check(): void {
		// disable NextGEN Gallery resource manager
		// if ( ! \defined( 'NGG_DISABLE_RESOURCE_MANAGER' ) ) {
		// \define( 'NGG_DISABLE_RESOURCE_MANAGER', true );
		// }.
	}

	/**
	 * Get all blog ids of blogs in the current network that are:
	 * - not archived
	 * - not spam
	 * - not deleted.
	 */
	private function get_blog_ids() {
		global $wpdb;

		// get an array of blog ids.
		$sql = "SELECT blog_id FROM $wpdb->blogs
      WHERE archived = '0' AND spam = '0'
      AND deleted = '0'";

		return $wpdb->get_col( $sql ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Static query, no user input
	}

	/**
	 * Get the role capability definition.
	 *
	 * @return array<string, array<string, bool>|array<int, string>> Role capabilities keyed by role.
	 */
	private static function role_capability_definition(): array {
		// WC 9.9 replaced promote_users with create_customers for customer creation.
		$customer_create_cap = \defined( 'WC_VERSION' ) && version_compare( WC_VERSION, '9.9', '>=' ) // @phpstan-ignore-line
			? 'create_customers'
			: 'promote_users';

		// Cashier role.
		$cashier_capabilities = array(
			'read'                      => true,
			'read_private_products'     => true,
			'publish_products'          => true,
			'edit_product'              => true,
			'edit_products'             => true,
			'edit_published_products'   => true,
			'edit_private_products'     => true,
			'edit_others_products'      => true,
			'read_private_shop_orders'  => true,
			'publish_shop_orders'       => true,
			'edit_shop_orders'          => true,
			'edit_others_shop_orders'   => true,
			'list_users'                => true,
			$customer_create_cap        => true,
			'edit_users'                => true,
			'read_private_shop_coupons' => true,
			'publish_shop_coupons'      => true,
			'edit_shop_coupons'         => true,
			'edit_published_shop_coupons' => true,
			'edit_private_shop_coupons' => true,
			'edit_others_shop_coupons'  => true,
			'manage_product_terms'      => true,
		);

		return array(
			'cashier'       => $cashier_capabilities,
			'administrator' => array(
				'manage_woocommerce_pos',
				'access_woocommerce_pos',
				'edit_wcpos_store',
				'read_wcpos_store',
				'delete_wcpos_store',
				'edit_wcpos_stores',
				'edit_others_wcpos_stores',
				'publish_wcpos_stores',
				'read_private_wcpos_stores',
				'delete_wcpos_stores',
				'delete_private_wcpos_stores',
				'delete_published_wcpos_stores',
				'delete_others_wcpos_stores',
				'edit_private_wcpos_stores',
				'edit_published_wcpos_stores',
			),
			'shop_manager'  => array( 'manage_woocommerce_pos', 'access_woocommerce_pos' ),
		);
	}

	/**
	 * Get the role-capabilities definition fingerprint.
	 */
	private function role_caps_fingerprint(): string {
		return md5( wp_json_encode( self::role_capability_definition() ) );
	}

	/**
	 * Add POS specific roles.
	 */
	private function create_pos_roles(): void {
		$role_capabilities    = self::role_capability_definition();
		$cashier_capabilities = $role_capabilities['cashier'];

		add_role(
			'cashier',
			/* translators: Plugin activation notice label. */
			__( 'Cashier', 'woocommerce-pos' ),
			$cashier_capabilities
		);

		$obsolete_customer_create_cap = isset( $cashier_capabilities['create_customers'] ) ? 'promote_users' : 'create_customers';
		$cashier                      = get_role( 'cashier' );
		if ( $cashier ) {
			$cashier->remove_cap( $obsolete_customer_create_cap );
		}

		// Sync all capabilities to the existing role. add_role() is a no-op when
		// the role already exists, so capabilities added in newer versions would
		// never reach existing installs without this.
		$this->add_pos_capability(
			array(
				'cashier' => array_merge(
					array( 'access_woocommerce_pos' ),
					array_keys( $cashier_capabilities )
				),
			)
		);
	}

	/**
	 * Add default pos capabilities to administrator and shop_manager roles.
	 *
	 * @param array $roles An array of arrays representing the roles and their POS capabilities.
	 */
	private function add_pos_capability( $roles ): void {
		foreach ( $roles as $slug => $caps ) {
			$role = get_role( $slug );
			if ( $role ) {
				foreach ( $caps as $cap ) {
					$role->add_cap( $cap );
				}
			}
		}
	}

	/**
	 * Upgrade database.
	 *
	 * @param string $old     Old version.
	 * @param string $current Current version.
	 */
	private function db_upgrade( $old, $current ): void {
		$db_updates = array(
			'0.4'          => 'updates/update-0.4.php',
			'0.4.6'        => 'updates/update-0.4.6.php',
			'1.0.0-beta.1' => 'updates/update-1.0.0-beta.1.php',
			'1.6.1'        => 'updates/update-1.6.1.php',
			'1.8.0'        => 'updates/update-1.8.0.php',
			'1.8.7'        => 'updates/update-1.8.7.php',
			'1.8.12'       => 'updates/update-1.8.12.php',
			'1.8.13'       => 'updates/update-1.8.13.php',
			'1.9.0'        => 'updates/update-1.9.0.php',
			'1.10.0'       => 'updates/update-1.10.0.php',
		);
		foreach ( $db_updates as $version => $updater ) {
			if ( version_compare( $version, $old, '>' ) &&
			 version_compare( $version, $current, '<=' ) ) {
				include $updater;
			}
		}

		if ( Sync_Api::SCHEMA_VERSION !== get_option( Sync_Api::SCHEMA_OPTION, null ) ) {
			$this->install_sync_schema();
		}

		// Installs that predate 2026-09 wrote the per-request latches with
		// autoload off; every upgrade re-asserts autoload so the flip is
		// idempotent and needs no versioned update file.
		self::autoload_request_latches();
		Admin\Permalink::ensure_default();
	}

	/**
	 * Every option row that is read on EVERY request and must therefore ride in
	 * alloptions: the three sync latches the Init constructor reads, the permalink
	 * slug Template_Router reads, and each registered settings section that
	 * declares {@see Services\Settings\Abstract_Section::autoload()} — the
	 * sections are the extension point, so Pro's and extensions' sections join
	 * the repair by declaring it, without touching this file.
	 *
	 * Needed because core's update_option() returns early on an unchanged value
	 * WITHOUT touching the autoload column, so a writer alone never repairs a row
	 * an older release wrote with autoload off. Without a persistent object cache
	 * each such row cost one `SELECT option_value` per page load (measured
	 * 2026-09-03 on dev-next and dev-free).
	 *
	 * @return string[]
	 */
	private static function request_option_names(): array {
		$names = array(
			Sync_Api::SCHEMA_OPTION,
			\WCPOS\WooCommercePOS\Sync\Visibility_Observer::SEED_VERSION_OPTION,
			\WCPOS\WooCommercePOS\Sync\Config_Fingerprint::CLEANUP_VERSION_OPTION,
			Admin\Permalink::DB_KEY,
		);
		foreach ( Services\Settings::instance()->sections()->all() as $section ) {
			if ( $section instanceof Services\Settings\Abstract_Section && $section->autoload() ) {
				$names[] = $section->autoload_option_name();
			}
		}
		return $names;
	}

	/**
	 * Flip the per-request rows to autoload in place, and seed the settings
	 * sections that are absent.
	 *
	 * One UPDATE on the flag column: never delete-and-recreate, because
	 * `Sync_Api::SCHEMA_OPTION` gates the sync observers on every request and a
	 * request landing in that gap would run with journaling off. 'yes' is
	 * accepted by every core version (6.6+ maps it alongside 'on'). Idempotent:
	 * already-autoloaded rows match nothing. Latches that were never written
	 * stay absent (their absence is the signal). An autoloaded settings section
	 * that was never saved is seeded as an autoloaded row — an absent option is
	 * queried on every request too. General also persists its migrated consent
	 * so the legacy row does not remain on the read path; other defaults stay
	 * dynamic.
	 */
	public static function autoload_request_latches(): void {
		global $wpdb;
		// The key comes from the section itself (autoload_option_name()): Pro's
		// License section stores under a Pro-prefixed key, so deriving it from
		// id() flipped nothing for that row and seeded a stray free-prefixed one.
		foreach ( Services\Settings::instance()->sections()->all() as $section ) {
			if ( ! $section instanceof Services\Settings\Abstract_Section || ! $section->autoload() ) {
				continue;
			}
			$option_name = $section->autoload_option_name();
			if ( false === get_option( $option_name ) ) {
				$value = $section instanceof Services\Settings\General_Section
					? array( 'tracking_consent' => $section->raw_tracking_consent() )
					: array();
				add_option( $option_name, $value, '', true );
			}
		}
		$options      = self::request_option_names();
		$placeholders = implode( ', ', array_fill( 0, \count( $options ), '%s' ) );
		$flipped      = $wpdb->query( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- the flag column is the target; caches are cleared below.
			$wpdb->prepare(
				"UPDATE {$wpdb->options} SET autoload = 'yes' WHERE option_name IN ({$placeholders}) AND autoload NOT IN ('yes', 'on', 'auto-on')", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- placeholders are generated for the prepared values.
				$options
			)
		);
		if ( ! $flipped ) {
			return;
		}
		foreach ( $options as $option ) {
			wp_cache_delete( $option, 'options' );
		}
		wp_cache_delete( 'alloptions', 'options' );
	}

	/**
	 * If \WCPOS\WooCommercePOSPro\ is installed, check the version is above MIN_PRO_VERSION.
	 */
	private function pro_version_check(): void {
		if ( class_exists( '\WCPOS\WooCommercePOSPro\Activator' ) ) {
			if ( version_compare( \WCPOS\WooCommercePOSPro\VERSION, MIN_PRO_VERSION, '<' ) ) { // @phpstan-ignore-line

				/*
				 * NOTE: the deactivate_plugins function is not available in the frontend or ajax
				 * This is an extreme situation where the Pro plugin could crash the site, so we need to deactivate it
				 */
				if ( ! \function_exists( 'deactivate_plugins' ) ) {
					require_once ABSPATH . '/wp-admin/includes/plugin.php';
				}

				// WCPOS Pro is activated, but the version is too low - use the constant for dynamic folder name.
				deactivate_plugins( \WCPOS\WooCommercePOSPro\PLUGIN_FILE ); // @phpstan-ignore-line

				// Defer __() call to avoid "too early" warning in WordPress 6.7+.
				add_action(
					'admin_init',
					function () {
						$message = \sprintf(
							// translators: 1: WCPOS Pro URL, 2: Minimum Pro version, 3: Plugins URL.
							__( '<strong>WCPOS</strong> requires <a href="%1$s">WCPOS Pro %2$s or higher</a>. Please <a href="%3$s">install and activate WCPOS Pro</a>', 'woocommerce-pos' ),
							'https://wcpos.com/my-account',
							MIN_PRO_VERSION,
							admin_url( 'plugins.php' )
						) . ' &raquo;';

						Admin\Notices::add( $message );
					}
				);
			}
		}
	}
}

```
