File "Admin.php"

Full Path: /home/vantageo/public_html/cache/cache/.wp-cli/wp-content/plugins/facebook-for-woocommerce/includes/Admin.php
File size: 55.61 KB
MIME-type: text/x-php
Charset: utf-8

<?php
// phpcs:ignoreFile
/**
 * Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved
 *
 * This source code is licensed under the license found in the
 * LICENSE file in the root directory of this source tree.
 *
 * @package FacebookCommerce
 */

namespace WooCommerce\Facebook;

use WooCommerce\Facebook\Admin\Enhanced_Catalog_Attribute_Fields;
use WooCommerce\Facebook\Framework\Helper;
use Automattic\WooCommerce\Utilities\OrderUtil;

defined( 'ABSPATH' ) or exit;

/**
 * Admin handler.
 *
 * @since 1.10.0
 */
class Admin {

	/** @var string the "sync and show" sync mode slug */
	const SYNC_MODE_SYNC_AND_SHOW = 'sync_and_show';

	/** @var string the "sync and show" sync mode slug */
	const SYNC_MODE_SYNC_AND_HIDE = 'sync_and_hide';

	/** @var string the "sync disabled" sync mode slug */
	const SYNC_MODE_SYNC_DISABLED = 'sync_disabled';

	/** @var Product_Categories the product category admin handler */
	protected $product_categories;

	/** @var array screens ids where to include scripts */
	protected $screen_ids = [];

	/** @var Product_Sets the product set admin handler. */
	protected $product_sets;

	/**
	 * Admin constructor.
	 *
	 * @since 1.10.0
	 */
	public function __construct() {

		$order_screen_id = class_exists( OrderUtil::class ) ? OrderUtil::get_order_admin_screen() : 'shop_order';

		$this->screen_ids = [
			'product',
			'edit-product',
			'woocommerce_page_wc-facebook',
			'marketing_page_wc-facebook',
			'edit-product_cat',
			$order_screen_id,
		];

		// enqueue admin scripts
		add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_scripts' ) );

		$plugin = facebook_for_woocommerce();
		// only alter the admin UI if the plugin is connected to Facebook and ready to sync products
		if ( ! $plugin->get_connection_handler()->is_connected() || ! $plugin->get_integration()->get_product_catalog_id() ) {
			return;
		}

		$this->product_categories = new Admin\Product_Categories();
		$this->product_sets       = new Admin\Product_Sets();
		// add a modal in admin product pages
		add_action( 'admin_footer', array( $this, 'render_modal_template' ) );

		// add admin notice to inform that disabled products may need to be deleted manually
		add_action( 'admin_notices', array( $this, 'maybe_show_product_disabled_sync_notice' ) );

		// add admin notice if the user is enabling sync for virtual products using the bulk action
		add_action( 'admin_notices', array( $this, 'maybe_add_enabling_virtual_products_sync_notice' ) );
		add_filter( 'request', array( $this, 'filter_virtual_products_affected_enabling_sync' ) );

		// add admin notice to inform sync mode has been automatically set to Sync and hide for virtual products and variations
		add_action( 'admin_notices', array( $this, 'add_handled_virtual_products_variations_notice' ) );

		// add columns for displaying Facebook sync enabled/disabled and catalog visibility status
		add_filter( 'manage_product_posts_columns', array( $this, 'add_product_list_table_columns' ) );
		add_action( 'manage_product_posts_custom_column', array( $this, 'add_product_list_table_columns_content' ) );

		// add input to filter products by Facebook sync enabled
		add_action( 'restrict_manage_posts', array( $this, 'add_products_by_sync_enabled_input_filter' ), 40 );
		add_filter( 'request', array( $this, 'filter_products_by_sync_enabled' ) );

		// add bulk actions to manage products sync
		add_filter( 'bulk_actions-edit-product', array( $this, 'add_products_sync_bulk_actions' ), 40 );
		add_action( 'handle_bulk_actions-edit-product', array( $this, 'handle_products_sync_bulk_actions' ) );

		// add Product data tab
		add_filter( 'woocommerce_product_data_tabs', array( $this, 'add_product_settings_tab' ) );
		add_action( 'woocommerce_product_data_panels', array( $this, 'add_product_settings_tab_content' ) );

		// add Variation edit fields
		add_action( 'woocommerce_product_after_variable_attributes', array( $this, 'add_product_variation_edit_fields' ), 10, 3 );
		add_action( 'woocommerce_save_product_variation', array( $this, 'save_product_variation_edit_fields' ), 10, 2 );

		// add custom taxonomy for Product Sets
		add_filter( 'gettext', array( $this, 'change_custom_taxonomy_tip' ), 20, 2 );
	}

	/**
	 * __get method for backward compatibility.
	 *
	 * @param string $key property name
	 * @return mixed
	 * @since 3.0.32
	 */
	public function __get( $key ) {
		// Add warning for private properties.
		if ( 'product_sets' === $key ) {
			/* translators: %s property name. */
			_doing_it_wrong( __FUNCTION__, sprintf( esc_html__( 'The %s property is protected and should not be accessed outside its class.', 'facebook-for-woocommerce' ), esc_html( $key ) ), '3.0.32' );
			return $this->$key;
		}

		return null;
	}

	/**
	 * Change custom taxonomy tip text
	 *
	 * @since 2.3.0
	 *
	 * @param string $translation Text translation.
	 * @param string $text Original text.
	 *
	 * @return string
	 */
	public function change_custom_taxonomy_tip( $translation, $text ) {
		global $current_screen;
		if ( isset( $current_screen->id ) && 'edit-fb_product_set' === $current_screen->id && 'The name is how it appears on your site.' === $text ) {
			$translation = esc_html__( 'The name is how it appears on Facebook Catalog.', 'facebook-for-woocommerce' );
		}
		return $translation;
	}

	/**
	 * Enqueues admin scripts.
	 *
	 * @internal
	 *
	 * @since 1.10.0
	 */
	public function enqueue_scripts() {
		global $current_screen;

		if ( isset( $current_screen->id ) ) {

			if ( in_array( $current_screen->id, $this->screen_ids, true ) || facebook_for_woocommerce()->is_plugin_settings() ) {

				// enqueue modal functions
				wp_enqueue_script(
					'facebook-for-woocommerce-modal',
					facebook_for_woocommerce()->get_asset_build_dir_url() . '/admin/modal.js',
					array( 'jquery', 'wc-backbone-modal', 'jquery-blockui' ),
					\WC_Facebookcommerce::PLUGIN_VERSION
				);

				// enqueue google product category select
				wp_enqueue_script(
					'wc-facebook-google-product-category-fields',
					facebook_for_woocommerce()->get_asset_build_dir_url() . '/admin/google-product-category-fields.js',
					array( 'jquery' ),
					\WC_Facebookcommerce::PLUGIN_VERSION
				);

				wp_localize_script(
					'wc-facebook-google-product-category-fields',
					'facebook_for_woocommerce_google_product_category',
					array(
						'i18n' => array(
							'top_level_dropdown_placeholder' => __( 'Search main categories...', 'facebook-for-woocommerce' ),
							'second_level_empty_dropdown_placeholder' => __( 'Choose a main category', 'facebook-for-woocommerce' ),
							'general_dropdown_placeholder'   => __( 'Choose a category', 'facebook-for-woocommerce' ),
						),
					)
				);
			}

			if ( 'edit-fb_product_set' === $current_screen->id ) {
				// enqueue WooCommerce Admin Styles because of Select2
				wp_enqueue_style(
					'woocommerce_admin_styles',
					WC()->plugin_url() . '/assets/css/admin.css',
					[],
					\WC_Facebookcommerce::PLUGIN_VERSION
				);
				wp_enqueue_style(
					'facebook-for-woocommerce-product-sets-admin',
					facebook_for_woocommerce()->get_plugin_url() . '/assets/css/admin/facebook-for-woocommerce-product-sets-admin.css',
					[],
					\WC_Facebookcommerce::PLUGIN_VERSION
				);

				wp_enqueue_script(
					'facebook-for-woocommerce-product-sets',
					facebook_for_woocommerce()->get_asset_build_dir_url() . '/admin/product-sets-admin.js',
					[ 'jquery', 'select2' ],
					\WC_Facebookcommerce::PLUGIN_VERSION,
					true
				);

				wp_localize_script(
					'facebook-for-woocommerce-product-sets',
					'facebook_for_woocommerce_product_sets',
					array(

						'excluded_category_ids'             => facebook_for_woocommerce()->get_integration()->get_excluded_product_category_ids(),
						'excluded_category_warning_message' => __( 'You have selected one or more categories currently excluded from the Facebook sync. Products belonging to the excluded categories will not be added to your Facebook Product Set.', 'facebook-for-woocommerce' ),
					)
				);
			}

			if ( 'product' === $current_screen->id || 'edit-product' === $current_screen->id ) {
				wp_enqueue_style(
					'facebook-for-woocommerce-products-admin',
					facebook_for_woocommerce()->get_plugin_url() . '/assets/css/admin/facebook-for-woocommerce-products-admin.css',
					[],
					\WC_Facebookcommerce::PLUGIN_VERSION
				);
				wp_enqueue_script(
					'facebook-for-woocommerce-products-admin',
					facebook_for_woocommerce()->get_asset_build_dir_url() . '/admin/products-admin.js',
					[ 'jquery', 'wc-backbone-modal', 'jquery-blockui', 'facebook-for-woocommerce-modal' ],
					\WC_Facebookcommerce::PLUGIN_VERSION
				);
				wp_localize_script(
					'facebook-for-woocommerce-products-admin',
					'facebook_for_woocommerce_products_admin',
					[
						'ajax_url'                                        => admin_url( 'admin-ajax.php' ),
						'enhanced_attribute_optional_selector'            => Enhanced_Catalog_Attribute_Fields::FIELD_ENHANCED_CATALOG_ATTRIBUTE_PREFIX . Enhanced_Catalog_Attribute_Fields::OPTIONAL_SELECTOR_KEY,
						'enhanced_attribute_page_type_edit_category'      => Enhanced_Catalog_Attribute_Fields::PAGE_TYPE_EDIT_CATEGORY,
						'enhanced_attribute_page_type_add_category'       => Enhanced_Catalog_Attribute_Fields::PAGE_TYPE_ADD_CATEGORY,
						'enhanced_attribute_page_type_edit_product'       => Enhanced_Catalog_Attribute_Fields::PAGE_TYPE_EDIT_PRODUCT,
						'is_product_published'                            => $this->is_current_product_published(),
						'is_sync_enabled_for_product'                     => $this->is_sync_enabled_for_current_product(),
						'set_product_visibility_nonce'                    => wp_create_nonce( 'set-products-visibility' ),
						'set_product_sync_prompt_nonce'                   => wp_create_nonce( 'set-product-sync-prompt' ),
						'set_product_sync_bulk_action_prompt_nonce'       => wp_create_nonce( 'set-product-sync-bulk-action-prompt' ),
						'product_not_ready_modal_message'                 => $this->get_product_not_ready_modal_message(),
						'product_not_ready_modal_buttons'                 => $this->get_product_not_ready_modal_buttons(),
						'product_removed_from_sync_confirm_modal_message' => $this->get_product_removed_from_sync_confirm_modal_message(),
						'product_removed_from_sync_confirm_modal_buttons' => $this->get_product_removed_from_sync_confirm_modal_buttons(),
						'product_removed_from_sync_field_id'              => '#' . \WC_Facebook_Product::FB_REMOVE_FROM_SYNC,
						'i18n'                                            => [
							'missing_google_product_category_message' => __( 'Please enter a Google product category and at least one sub-category to sell this product on Instagram.', 'facebook-for-woocommerce' ),
						],
					]
				);
			}//end if

			if ( facebook_for_woocommerce()->is_plugin_settings() ) {
				wp_enqueue_style( 'woocommerce_admin_styles' );
				wp_enqueue_script( 'wc-enhanced-select' );
			}
		}//end if

	}

	/**
	 * Determines whether sync is enabled for the current product.
	 *
	 * @since 2.0.5
	 *
	 * @return bool
	 */
	private function is_sync_enabled_for_current_product() {
		global $post;
		$product = wc_get_product( $post );
		if ( ! $product instanceof \WC_Product ) {
			return false;
		}
		return Products::is_sync_enabled_for_product( $product );
	}

	/**
	 * Determines whether the current product is published.
	 *
	 * @since 2.6.15
	 *
	 * @return bool
	 */
	private function is_current_product_published() {
		global $post;
		$product = wc_get_product( $post );
		if ( ! $product instanceof \WC_Product ) {
			return false;
		}
		return 'publish' === $product->get_status();
	}

	/**
	 * Gets the markup for the message used in the product not ready modal.
	 *
	 * @since 2.1.0
	 *
	 * @return string
	 */
	private function get_product_not_ready_modal_message() {
		ob_start();
		?>
		<p><?php esc_html_e( 'To sell this product on Instagram, please ensure it meets the following requirements:', 'facebook-for-woocommerce' ); ?></p>
		<ul class="ul-disc">
			<li><?php esc_html_e( 'Has a price defined', 'facebook-for-woocommerce' ); ?></li>
			<li>
			<?php
			echo esc_html(
				sprintf(
				/* translators: Placeholders: %1$s - <strong> opening HTML tag, %2$s - </strong> closing HTML tag */
					__( 'Has %1$sManage Stock%2$s enabled on the %1$sInventory%2$s tab', 'facebook-for-woocommerce' ),
					'<strong>',
					'</strong>'
				)
			);
			?>
			</li>
			<li>
			<?php
			echo esc_html(
				sprintf(
				/* translators: Placeholders: %1$s - <strong> opening HTML tag, %2$s - </strong> closing HTML tag */
					__( 'Has the %1$sFacebook Sync%2$s setting set to "Sync and show" or "Sync and hide"', 'facebook-for-woocommerce' ),
					'<strong>',
					'</strong>'
				)
			);
			?>
			</li>
		</ul>
		<?php
		return ob_get_clean();
	}

	/**
	 * Gets the markup for the buttons used in the product not ready modal.
	 *
	 * @since 2.1.0
	 *
	 * @return string
	 */
	private function get_product_not_ready_modal_buttons() {
		ob_start();
		?>
		<button
			id="btn-ok"
			class="button button-large button-primary"
		><?php esc_html_e( 'Close', 'facebook-for-woocomerce' ); ?></button>
		<?php
		return ob_get_clean();
	}

	/**
	 * Gets the markup for the message used in the product removed from sync confirm modal.
	 *
	 * @internal
	 *
	 * @since 2.3.0
	 *
	 * @return string
	 */
	private function get_product_removed_from_sync_confirm_modal_message() {
		ob_start();
		?>
		<p>
		<?php
		printf(
			/* translators: Placeholders: %1$s - opening <a> link tag, %2$s - closing </a> link tag */
			esc_html__( 'You\'re removing a product from the Facebook sync that is currently listed in your %1$sFacebook catalog%2$s. Would you like to delete the product from the Facebook catalog as well?', 'facebook-for-woocommerce' ),
			'<a href="https://www.facebook.com/products" target="_blank">',
			'</a>'
		);
		?>
			</p>
		<?php
		return ob_get_clean();
	}

	/**
	 * Gets the markup for the buttons used in the product removed from sync confirm modal.
	 *
	 * @internal
	 *
	 * @since 2.3.0
	 *
	 * @return string
	 */
	private function get_product_removed_from_sync_confirm_modal_buttons() {
		ob_start();
		?>
		<button
			id="btn-ok"
			class="button button-large button-primary"
		><?php esc_html_e( 'Remove from sync only', 'facebook-for-woocomerce' ); ?></button>

		<button
			class="button button-large button-delete button-product-removed-from-sync-delete"
		><?php esc_html_e( 'Remove from sync and delete', 'facebook-for-woocomerce' ); ?></button>

		<button
			class="button button-large button-product-removed-from-sync-cancel"
		><?php esc_html_e( 'Cancel', 'facebook-for-woocomerce' ); ?></button>
		<?php
		return ob_get_clean();
	}

	/**
	 * Gets the product category admin handler instance.
	 *
	 * @since 2.1.0
	 *
	 * @return Product_Categories
	 */
	public function get_product_categories_handler() {
		return $this->product_categories;
	}

	/**
	 * Adds Facebook-related columns in the products edit screen.
	 *
	 * @internal
	 *
	 * @since 1.10.0
	 *
	 * @param array $columns array of keys and labels
	 * @return array
	 */
	public function add_product_list_table_columns( $columns ) {
		$columns['facebook_sync'] = __( 'Facebook sync', 'facebook-for-woocommerce' );
		return $columns;
	}

	/**
	 * Outputs sync information for products in the edit screen.
	 *
	 * @internal
	 *
	 * @since 1.10.0
	 *
	 * @param string $column the current column in the posts table
	 */
	public function add_product_list_table_columns_content( $column ) {
		global $post;

		if ( 'facebook_sync' !== $column ) {
			return;
		}

		$product        = wc_get_product( $post );
		$should_sync    = false;
		$no_sync_reason = '';

		if ( $product instanceof \WC_Product ) {
			try {
				facebook_for_woocommerce()->get_product_sync_validator( $product )->validate();
				$should_sync = true;
			} catch ( \Exception $e ) {
				$no_sync_reason = $e->getMessage();
			}
		}

		if ( $should_sync ) {
			if ( Products::is_product_visible( $product ) ) {
				esc_html_e( 'Sync and show', 'facebook-for-woocommerce' );
			} else {
				esc_html_e( 'Sync and hide', 'facebook-for-woocommerce' );
			}
		} else {
			esc_html_e( 'Do not sync', 'facebook-for-woocommerce' );
			if ( ! empty( $no_sync_reason ) ) {
				echo wc_help_tip( $no_sync_reason );
			}
		}
	}

	/**
	 * Adds a dropdown input to let shop managers filter products by sync setting.
	 *
	 * @internal
	 *
	 * @since 1.10.0
	 */
	public function add_products_by_sync_enabled_input_filter() {
		global $typenow;

		if ( 'product' !== $typenow ) {
			return;
		}

		// phpcs:ignore WordPress.Security.NonceVerification.Recommended
		$choice = isset( $_GET['fb_sync_enabled'] ) ? (string) sanitize_text_field( wp_unslash( $_GET['fb_sync_enabled'] ) ) : '';
		?>
		<select name="fb_sync_enabled">
			<option value="" <?php selected( $choice, '' ); ?>><?php esc_html_e( 'Filter by Facebook sync setting', 'facebook-for-woocommerce' ); ?></option>
			<option value="<?php echo esc_attr( self::SYNC_MODE_SYNC_AND_SHOW ); ?>" <?php selected( $choice, self::SYNC_MODE_SYNC_AND_SHOW ); ?>><?php esc_html_e( 'Sync and show', 'facebook-for-woocommerce' ); ?></option>
			<option value="<?php echo esc_attr( self::SYNC_MODE_SYNC_AND_HIDE ); ?>" <?php selected( $choice, self::SYNC_MODE_SYNC_AND_HIDE ); ?>><?php esc_html_e( 'Sync and hide', 'facebook-for-woocommerce' ); ?></option>
			<option value="<?php echo esc_attr( self::SYNC_MODE_SYNC_DISABLED ); ?>" <?php selected( $choice, self::SYNC_MODE_SYNC_DISABLED ); ?>><?php esc_html_e( 'Do not sync', 'facebook-for-woocommerce' ); ?></option>
		</select>
		<?php
	}

	/**
	 * Filters products by Facebook sync setting.
	 *
	 * @internal
	 *
	 * @since 1.10.0
	 *
	 * @param array $query_vars product query vars for the edit screen
	 * @return array
	 */
	public function filter_products_by_sync_enabled( $query_vars ) {
		$valid_values = array(
			self::SYNC_MODE_SYNC_AND_SHOW,
			self::SYNC_MODE_SYNC_AND_HIDE,
			self::SYNC_MODE_SYNC_DISABLED,
		);

		// phpcs:ignore WordPress.Security.NonceVerification.Recommended
		if ( isset( $_REQUEST['fb_sync_enabled'] ) && in_array( $_REQUEST['fb_sync_enabled'], $valid_values, true ) ) {
			// store original meta query
			$original_meta_query = ! empty( $query_vars['meta_query'] ) ? $query_vars['meta_query'] : [];
			// phpcs:ignore WordPress.Security.NonceVerification.Recommended
			$filter_value = wc_clean( wp_unslash( $_REQUEST['fb_sync_enabled'] ) );
			// by default use an "AND" clause if multiple conditions exist for a meta query
			if ( ! empty( $query_vars['meta_query'] ) ) {
				$query_vars['meta_query']['relation'] = 'AND';
			} else {
				$query_vars['meta_query'] = [];
			}

			if ( self::SYNC_MODE_SYNC_AND_SHOW === $filter_value ) {
				// when checking for products with sync enabled we need to check both "yes" and meta not set, this requires adding an "OR" clause
				$query_vars = $this->add_query_vars_to_find_products_with_sync_enabled( $query_vars );
				// only get visible products (both "yes" and meta not set)
				$query_vars = $this->add_query_vars_to_find_visible_products( $query_vars );
				// since we record enabled status and visibility on child variations, we need to query variable products found for their children to exclude them from query results
				$exclude_products = [];
				$found_ids        = get_posts( array_merge( $query_vars, array( 'fields' => 'ids' ) ) );
				$found_products   = empty( $found_ids ) ? [] : wc_get_products(
					array(
						'limit'   => -1,
						'type'    => 'variable',
						'include' => $found_ids,
					)
				);
				/** @var \WC_Product[] $found_products */
				foreach ( $found_products as $product ) {
					if ( ! Products::is_sync_enabled_for_product( $product )
						 || ! Products::is_product_visible( $product ) ) {
						$exclude_products[] = $product->get_id();
					}
				}

				if ( ! empty( $exclude_products ) ) {
					if ( ! empty( $query_vars['post__not_in'] ) ) {
						$query_vars['post__not_in'] = array_merge( $query_vars['post__not_in'], $exclude_products );
					} else {
						$query_vars['post__not_in'] = $exclude_products;
					}
				}
			} elseif ( self::SYNC_MODE_SYNC_AND_HIDE === $filter_value ) {
				// when checking for products with sync enabled we need to check both "yes" and meta not set, this requires adding an "OR" clause
				$query_vars = $this->add_query_vars_to_find_products_with_sync_enabled( $query_vars );
				// only get hidden products
				$query_vars = $this->add_query_vars_to_find_hidden_products( $query_vars );
				// since we record enabled status and visibility on child variations, we need to query variable products found for their children to exclude them from query results
				$exclude_products = [];
				$found_ids        = get_posts( array_merge( $query_vars, array( 'fields' => 'ids' ) ) );
				$found_products   = empty( $found_ids ) ? [] : wc_get_products(
					array(
						'limit'   => -1,
						'type'    => 'variable',
						'include' => $found_ids,
					)
				);
				/** @var \WC_Product[] $found_products */
				foreach ( $found_products as $product ) {
					if ( ! Products::is_sync_enabled_for_product( $product )
						 || Products::is_product_visible( $product ) ) {
						$exclude_products[] = $product->get_id();
					}
				}

				if ( ! empty( $exclude_products ) ) {
					if ( ! empty( $query_vars['post__not_in'] ) ) {
						$query_vars['post__not_in'] = array_merge( $query_vars['post__not_in'], $exclude_products );
					} else {
						$query_vars['post__not_in'] = $exclude_products;
					}
				}

				// for the same reason, we also need to include variable products with hidden children
				$include_products  = [];
				$hidden_variations = get_posts(
					array(
						'limit'      => -1,
						'post_type'  => 'product_variation',
						'meta_query' => array(
							'key'   => Products::VISIBILITY_META_KEY,
							'value' => 'no',
						),
					)
				);

				/** @var \WP_Post[] $hidden_variations */
				foreach ( $hidden_variations as $variation_post ) {
					$variable_product = wc_get_product( $variation_post->post_parent );
					// we need this check because we only want products with ALL variations hidden
					if ( $variable_product instanceof \WC_Product && Products::is_sync_enabled_for_product( $variable_product )
						 && ! Products::is_product_visible( $variable_product ) ) {
						$include_products[] = $variable_product->get_id();
					}
				}
			} else {
				// self::SYNC_MODE_SYNC_DISABLED
				// products to be included in the QUERY, not in the sync
				$include_products = [];
				$found_ids        = [];
				$integration             = facebook_for_woocommerce()->get_integration();
				$excluded_categories_ids = $integration ? $integration->get_excluded_product_category_ids() : [];
				$excluded_tags_ids       = $integration ? $integration->get_excluded_product_tag_ids() : [];
				// get the product IDs from all products in excluded taxonomies
				if ( $excluded_categories_ids || $excluded_tags_ids ) {
					$tax_query_vars   = $this->maybe_add_tax_query_for_excluded_taxonomies( $query_vars, true );
					$include_products = array_merge( $include_products, get_posts( array_merge( $tax_query_vars, array( 'fields' => 'ids' ) ) ) );
				}
				$excluded_products = get_posts(
					array(
						'fields'     => 'ids',
						'limit'      => -1,
						'post_type'  => 'product',
						'meta_query' => array(
							array(
								'key'   => Products::SYNC_ENABLED_META_KEY,
								'value' => 'no',
							),
						),
					)
				);
				$include_products = array_unique( array_merge( $include_products, $excluded_products ) );
				// since we record enabled status and visibility on child variations,
				// we need to include variable products with excluded children
				$excluded_variations = get_posts(
					array(
						'limit'      => -1,
						'post_type'  => 'product_variation',
						'meta_query' => array(
							array(
								'key'   => Products::SYNC_ENABLED_META_KEY,
								'value' => 'no',
							),
						),
					)
				);
				/** @var \WP_Post[] $excluded_variations */
				foreach ( $excluded_variations as $variation_post ) {
					$variable_product = wc_get_product( $variation_post->post_parent );
					// we need this check because we only want products with ALL variations excluded
					if ( ! Products::is_sync_enabled_for_product( $variable_product ) ) {
						$include_products[] = $variable_product->get_id();
					}
				}
			}//end if

			if ( ! empty( $include_products ) ) {
				// we are going to query by ID, so we want to include all the IDs from before
				$include_products = array_unique( array_merge( $found_ids, $include_products ) );
				if ( ! empty( $query_vars['post__in'] ) ) {
					$query_vars['post__in'] = array_merge( $query_vars['post__in'], $include_products );
				} else {
					$query_vars['post__in'] = $include_products;
				}

				// remove sync enabled and visibility meta queries
				if ( ! empty( $original_meta_query ) ) {
					$query_vars['meta_query'] = $original_meta_query;
				} else {
					unset( $query_vars['meta_query'] );
				}
			}
		}//end if

		if ( isset( $query_vars['meta_query'] ) && empty( $query_vars['meta_query'] ) ) {
			unset( $query_vars['meta_query'] );
		}

		return $query_vars;
	}


	/**
	 * Adds query vars to limit the results to products that have sync enabled.
	 *
	 * @since 1.10.0
	 *
	 * @param array $query_vars
	 * @return array
	 */
	private function add_query_vars_to_find_products_with_sync_enabled( array $query_vars ) {
		$meta_query = array(
			'relation' => 'OR',
			array(
				'key'   => Products::SYNC_ENABLED_META_KEY,
				'value' => 'yes',
			),
			array(
				'key'     => Products::SYNC_ENABLED_META_KEY,
				'compare' => 'NOT EXISTS',
			),
		);

		if ( empty( $query_vars['meta_query'] ) ) {
			$query_vars['meta_query'] = $meta_query;
		} elseif ( is_array( $query_vars['meta_query'] ) ) {
			$original_meta_query      = $query_vars['meta_query'];
			$query_vars['meta_query'] = array(
				'relation' => 'AND',
				$original_meta_query,
				$meta_query,
			);
		}

		// check whether the product belongs to an excluded product category or tag
		$query_vars = $this->maybe_add_tax_query_for_excluded_taxonomies( $query_vars );
		return $query_vars;
	}


	/**
	 * Adds a tax query to filter in/out products in excluded product categories and product tags.
	 *
	 * @since 1.10.0
	 *
	 * @param array $query_vars product query vars for the edit screen
	 * @param bool  $in whether we want to return products in excluded categories and tags or not
	 * @return array
	 */
	private function maybe_add_tax_query_for_excluded_taxonomies( $query_vars, $in = false ) {
		$integration = facebook_for_woocommerce()->get_integration();
		if ( $integration ) {
			$tax_query               = [];
			$excluded_categories_ids = $integration->get_excluded_product_category_ids();
			if ( $excluded_categories_ids ) {
				$tax_query[] = array(
					'taxonomy' => 'product_cat',
					'terms'    => $excluded_categories_ids,
					'field'    => 'term_id',
					'operator' => $in ? 'IN' : 'NOT IN',
				);
			}
			$excluded_tags_ids = $integration->get_excluded_product_tag_ids();
			if ( $excluded_tags_ids ) {
				$tax_query[] = array(
					'taxonomy' => 'product_tag',
					'terms'    => $excluded_tags_ids,
					'field'    => 'term_id',
					'operator' => $in ? 'IN' : 'NOT IN',
				);
			}

			if ( count( $tax_query ) > 1 ) {
				$tax_query['relation'] = $in ? 'OR' : 'AND';
			}

			if ( $tax_query && empty( $query_vars['tax_query'] ) ) {
				$query_vars['tax_query'] = $tax_query;
			} elseif ( $tax_query && is_array( $query_vars ) ) {
				$query_vars['tax_query'][] = $tax_query;
			}
		}//end if

		return $query_vars;
	}


	/**
	 * Adds query vars to limit the results to visible products.
	 *
	 * @since 2.0.0
	 *
	 * @param array $query_vars
	 * @return array
	 */
	private function add_query_vars_to_find_visible_products( array $query_vars ) {
		$visibility_meta_query = array(
			'relation' => 'OR',
			array(
				'key'   => Products::VISIBILITY_META_KEY,
				'value' => 'yes',
			),
			array(
				'key'     => Products::VISIBILITY_META_KEY,
				'compare' => 'NOT EXISTS',
			),
		);

		if ( empty( $query_vars['meta_query'] ) ) {
			$query_vars['meta_query'] = $visibility_meta_query;
		} elseif ( is_array( $query_vars['meta_query'] ) ) {
			$enabled_meta_query       = $query_vars['meta_query'];
			$query_vars['meta_query'] = array(
				'relation' => 'AND',
				$enabled_meta_query,
				$visibility_meta_query,
			);
		}

		return $query_vars;
	}


	/**
	 * Adds query vars to limit the results to hidden products.
	 *
	 * @since 2.0.0
	 *
	 * @param array $query_vars
	 * @return array
	 */
	private function add_query_vars_to_find_hidden_products( array $query_vars ) {
		$visibility_meta_query = array(
			'key'   => Products::VISIBILITY_META_KEY,
			'value' => 'no',
		);

		if ( empty( $query_vars['meta_query'] ) ) {
			$query_vars['meta_query'] = $visibility_meta_query;
		} elseif ( is_array( $query_vars['meta_query'] ) ) {
			$enabled_meta_query       = $query_vars['meta_query'];
			$query_vars['meta_query'] = array(
				'relation' => 'AND',
				$enabled_meta_query,
				$visibility_meta_query,
			);
		}

		return $query_vars;
	}


	/**
	 * Adds bulk actions in the products edit screen.
	 *
	 * @internal
	 *
	 * @since 1.10.0
	 *
	 * @param array $bulk_actions array of bulk action keys and labels
	 * @return array
	 */
	public function add_products_sync_bulk_actions( $bulk_actions ) {
		$bulk_actions['facebook_include'] = __( 'Include in Facebook sync', 'facebook-for-woocommerce' );
		$bulk_actions['facebook_exclude'] = __( 'Exclude from Facebook sync', 'facebook-for-woocommerce' );
		return $bulk_actions;
	}


	/**
	 * Handles a Facebook product sync bulk action.
	 *
	 * @internal
	 *
	 * @since 1.10.0
	 *
	 * @param string $redirect admin URL used by WordPress to redirect after performing the bulk action
	 * @return string
	 */
	public function handle_products_sync_bulk_actions( $redirect ) {

		// primary dropdown at the top of the list table
		// phpcs:ignore WordPress.Security.NonceVerification.Recommended
		$action = isset( $_REQUEST['action'] ) && -1 !== (int) $_REQUEST['action'] ? sanitize_text_field( wp_unslash( $_REQUEST['action'] ) ) : null;

		// secondary dropdown at the bottom of the list table
		if ( ! $action ) {
			// phpcs:ignore WordPress.Security.NonceVerification.Recommended
			$action = isset( $_REQUEST['action2'] ) && -1 !== (int) $_REQUEST['action2'] ? sanitize_text_field( wp_unslash( $_REQUEST['action2'] ) ) : null;
		}

		if ( $action && in_array( $action, array( 'facebook_include', 'facebook_exclude' ), true ) ) {
			$products = [];
			// phpcs:ignore WordPress.Security.NonceVerification.Recommended
			$product_ids = isset( $_REQUEST['post'] ) && is_array( $_REQUEST['post'] ) ? array_map( 'absint', $_REQUEST['post'] ) : [];
			if ( ! empty( $product_ids ) ) {
				/** @var \WC_Product[] $enabling_sync_virtual_products virtual products that are being included */
				$enabling_sync_virtual_products = [];
				/** @var \WC_Product_Variation[] $enabling_sync_virtual_variations virtual variations that are being included */
				$enabling_sync_virtual_variations = [];
				foreach ( $product_ids as $product_id ) {
					if ( $product = wc_get_product( $product_id ) ) {
						$products[] = $product;
						if ( 'facebook_include' === $action ) {
							if ( $product->is_virtual() && ! Products::is_sync_enabled_for_product( $product ) ) {
								$enabling_sync_virtual_products[ $product->get_id() ] = $product;
							} else {
								if ( $product->is_type( 'variable' ) ) {
									// collect the virtual variations
									foreach ( $product->get_children() as $variation_id ) {
										$variation = wc_get_product( $variation_id );
										if ( $variation && $variation->is_virtual() && ! Products::is_sync_enabled_for_product( $variation ) ) {
											$enabling_sync_virtual_products[ $product->get_id() ]     = $product;
											$enabling_sync_virtual_variations[ $variation->get_id() ] = $variation;
										}
									}
								}
							}//end if
						}//end if
					}//end if
				}//end foreach

				if ( ! empty( $enabling_sync_virtual_products ) || ! empty( $enabling_sync_virtual_variations ) ) {
					// display notice if enabling sync for virtual products or variations
					set_transient( 'wc_' . facebook_for_woocommerce()->get_id() . '_enabling_virtual_products_sync_show_notice_' . get_current_user_id(), true, 15 * MINUTE_IN_SECONDS );
					set_transient( 'wc_' . facebook_for_woocommerce()->get_id() . '_enabling_virtual_products_sync_affected_products_' . get_current_user_id(), array_keys( $enabling_sync_virtual_products ), 15 * MINUTE_IN_SECONDS );

					// set visibility for virtual products
					foreach ( $enabling_sync_virtual_products as $product ) {

						// do not set visibility for variable products
						if ( ! $product->is_type( 'variable' ) ) {
							Products::set_product_visibility( $product, false );
						}
					}

					// set visibility for virtual variations
					foreach ( $enabling_sync_virtual_variations as $variation ) {

						Products::set_product_visibility( $variation, false );
					}
				}//end if

				if ( 'facebook_include' === $action ) {

					Products::enable_sync_for_products( $products );

					$this->resync_products( $products );

				} elseif ( 'facebook_exclude' === $action ) {

					Products::disable_sync_for_products( $products );

					self::add_product_disabled_sync_notice( count( $products ) );
				}
			}//end if
		}//end if

		return $redirect;
	}


	/**
	 * Re-syncs the given products.
	 *
	 * @since 2.0.0
	 *
	 * @param \WC_Product $products
	 */
	private function resync_products( array $products ) {

		$integration = facebook_for_woocommerce()->get_integration();

		// re-sync each product
		foreach ( $products as $product ) {

			if ( $product->is_type( 'variable' ) ) {

				// create product group and schedule product variations to be synced in the background
				$integration->on_product_publish( $product->get_id() );

			} elseif ( $integration->product_should_be_synced( $product ) ) {

				// schedule simple products to be updated or deleted from the catalog in the background
				if ( Products::product_should_be_deleted( $product ) ) {
					facebook_for_woocommerce()->get_products_sync_handler()->delete_products( array( $product->get_id() ) );
				} else {
					facebook_for_woocommerce()->get_products_sync_handler()->create_or_update_products( array( $product->get_id() ) );
				}
			}
		}
	}


	/**
	 * Adds a transient so an informational notice is displayed on the next page load.
	 *
	 * @since 2.0.0
	 *
	 * @param int $count number of products
	 */
	public static function add_product_disabled_sync_notice( $count = 1 ) {

		if ( ! facebook_for_woocommerce()->get_admin_notice_handler()->is_notice_dismissed( 'wc-' . facebook_for_woocommerce()->get_id_dasherized() . '-product-disabled-sync' ) ) {
			set_transient( 'wc_' . facebook_for_woocommerce()->get_id() . '_show_product_disabled_sync_notice_' . get_current_user_id(), $count, MINUTE_IN_SECONDS );
		}
	}


	/**
	 * Adds a message for after a product or set of products get excluded from sync.
	 *
	 * @since 2.0.0
	 */
	public function maybe_show_product_disabled_sync_notice() {

		$transient_name = 'wc_' . facebook_for_woocommerce()->get_id() . '_show_product_disabled_sync_notice_' . get_current_user_id();
		$message_id     = 'wc-' . facebook_for_woocommerce()->get_id_dasherized() . '-product-disabled-sync';

		if ( ( $count = get_transient( $transient_name ) ) && ( Helper::is_current_screen( 'edit-product' ) || Helper::is_current_screen( 'product' ) ) ) {

			$message = sprintf(
				/* translators: Placeholders: %1$s - <strong> tag, %2$s - </strong> tag, %3$s - <a> tag, %4$s - <a> tag */
				_n( '%1$sHeads up!%2$s If this product was previously visible in Facebook, you may need to delete it from the %3$sFacebook catalog%4$s to completely hide it from customer view.', '%1$sHeads up!%2$s If these products were previously visible in Facebook, you may need to delete them from the %3$sFacebook catalog%4$s to completely hide them from customer view.', $count, 'facebook-for-woocommerce' ),
				'<strong>',
				'</strong>',
				'<a href="https://facebook.com/products" target="_blank">',
				'</a>'
			);

			$message .= '<a class="button js-wc-plugin-framework-notice-dismiss">' . esc_html__( "Don't show this notice again", 'facebook-for-woocommerce' ) . '</a>';

			facebook_for_woocommerce()->get_admin_notice_handler()->add_admin_notice(
				$message,
				$message_id,
				array(
					'dismissible' => false,
					// we add our own dismiss button
														'notice_class' => 'notice-info',
				)
			);

			delete_transient( $transient_name );
		}//end if
	}


	/**
	 * Prints a notice on products page to inform users that the virtual products selected for the Include bulk action will have sync enabled, but will be hidden.
	 *
	 * @internal
	 *
	 * @since 1.11.3-dev.2
	 */
	public function maybe_add_enabling_virtual_products_sync_notice() {

		$show_notice_transient_name       = 'wc_' . facebook_for_woocommerce()->get_id() . '_enabling_virtual_products_sync_show_notice_' . get_current_user_id();
		$affected_products_transient_name = 'wc_' . facebook_for_woocommerce()->get_id() . '_enabling_virtual_products_sync_affected_products_' . get_current_user_id();

		if ( Helper::is_current_screen( 'edit-product' ) && get_transient( $show_notice_transient_name ) && ( $affected_products = get_transient( $affected_products_transient_name ) ) ) {

			$message = sprintf(
				esc_html(
				/* translators: Placeholders: %1$s - number of affected products, %2$s opening HTML <a> tag, %3$s - closing HTML </a> tag, %4$s - opening HTML <a> tag, %5$s - closing HTML </a> tag */
					_n(
						'%2$s%1$s product%3$s or some of its variations could not be updated to show in the Facebook catalog — %4$sFacebook Commerce Policies%5$s prohibit selling some product types (like virtual products). You may still advertise Virtual products on Facebook.',
						'%2$s%1$s products%3$s or some of their variations could not be updated to show in the Facebook catalog — %4$sFacebook Commerce Policies%5$s prohibit selling some product types (like virtual products). You may still advertise Virtual products on Facebook.',
						count( $affected_products ),
						'facebook-for-woocommerce'
					)
				),
				count( $affected_products ),
				'<a href="' . esc_url( add_query_arg( array( 'facebook_show_affected_products' => 1 ) ) ) . '">',
				'</a>',
				'<a href="https://www.facebook.com/policies/commerce/prohibited_content/subscriptions_and_digital_products" target="_blank">',
				'</a>'
			);

			facebook_for_woocommerce()->get_admin_notice_handler()->add_admin_notice(
				$message,
				'wc-' . facebook_for_woocommerce()->get_id_dasherized() . '-enabling-virtual-products-sync',
				array(
					'dismissible'  => false,
					'notice_class' => 'notice-info',
				)
			);

			delete_transient( $show_notice_transient_name );
		}//end if
	}


	/**
	 * Tweaks the query to show a filtered view with the affected products.
	 *
	 * @internal
	 *
	 * @since 2.0.0
	 *
	 * @param array $query_vars product query vars for the edit screen
	 * @return array
	 */
	public function filter_virtual_products_affected_enabling_sync( $query_vars ) {

		$transient_name = 'wc_' . facebook_for_woocommerce()->get_id() . '_enabling_virtual_products_sync_affected_products_' . get_current_user_id();

		if ( isset( $_GET['facebook_show_affected_products'] ) && Helper::is_current_screen( 'edit-product' ) && $affected_products = get_transient( $transient_name ) ) {

			$query_vars['post__in'] = $affected_products;
		}

		return $query_vars;
	}


	/**
	 * Prints a notice to inform sync mode has been automatically set to Sync and hide for virtual products and variations.
	 *
	 * @internal
	 *
	 * @since 2.0.0
	 */
	public function add_handled_virtual_products_variations_notice() {

		if ( 'yes' === get_option( 'wc_facebook_background_handle_virtual_products_variations_complete', 'no' ) &&
			 'yes' !== get_option( 'wc_facebook_background_handle_virtual_products_variations_skipped', 'no' ) ) {

			facebook_for_woocommerce()->get_admin_notice_handler()->add_admin_notice(
				sprintf(
					/* translators: Placeholders: %1$s - opening HTML <strong> tag, %2$s - closing HTML </strong> tag, %3$s - opening HTML <a> tag, %4$s - closing HTML </a> tag */
					esc_html__( '%1$sHeads up!%2$s Facebook\'s %3$sCommerce Policies%4$s do not support selling virtual products, so we have hidden your synced Virtual products in your Facebook catalog. You may still advertise Virtual products on Facebook.', 'facebook-for-woocommerce' ),
					'<strong>',
					'</strong>',
					'<a href="https://www.facebook.com/policies/commerce/prohibited_content/subscriptions_and_digital_products" target="_blank">',
					'</a>'
				),
				'wc-' . facebook_for_woocommerce()->get_id_dasherized() . '-updated-virtual-products-sync',
				array(
					'notice_class'            => 'notice-info',
					'always_show_on_settings' => false,
				)
			);
		}
	}


	/**
	 * Adds a new tab to the Product edit page.
	 *
	 * @internal
	 *
	 * @since 1.10.0
	 *
	 * @param array $tabs product tabs
	 * @return array
	 */
	public function add_product_settings_tab( $tabs ) {

		$tabs['fb_commerce_tab'] = array(
			'label'  => __( 'Facebook', 'facebook-for-woocommerce' ),
			'target' => 'facebook_options',
			'class'  => array( 'show_if_simple', 'show_if_variable', 'show_if_external' ),
		);

		return $tabs;
	}


	/**
	 * Adds content to the new Facebook tab on the Product edit page.
	 *
	 * @internal
	 *
	 * @since 1.10.0
	 */
	public function add_product_settings_tab_content() {
		global $post;


		// all products have sync enabled unless explicitly disabled
		$sync_enabled = 'no' !== get_post_meta( $post->ID, Products::SYNC_ENABLED_META_KEY, true );
		$is_visible   = ( $visibility = get_post_meta( $post->ID, Products::VISIBILITY_META_KEY, true ) ) ? wc_string_to_bool( $visibility ) : true;
		$product 	  = wc_get_product( $post );

		$description  = get_post_meta( $post->ID, \WC_Facebookcommerce_Integration::FB_PRODUCT_DESCRIPTION, true );
		$price        = get_post_meta( $post->ID, \WC_Facebook_Product::FB_PRODUCT_PRICE, true );
		$image_source = get_post_meta( $post->ID, Products::PRODUCT_IMAGE_SOURCE_META_KEY, true );
		$image        = get_post_meta( $post->ID, \WC_Facebook_Product::FB_PRODUCT_IMAGE, true );

		if ( $sync_enabled ) {
			$sync_mode = $is_visible ? self::SYNC_MODE_SYNC_AND_SHOW : self::SYNC_MODE_SYNC_AND_HIDE;
		} else {
			$sync_mode = self::SYNC_MODE_SYNC_DISABLED;
		}

		// 'id' attribute needs to match the 'target' parameter set above
		?>
		<div id='facebook_options' class='panel woocommerce_options_panel'>
			<div class='options_group hide_if_variable'>
				<?php

				woocommerce_wp_select(
					array(
						'id'      => 'wc_facebook_sync_mode',
						'label'   => __( 'Facebook sync', 'facebook-for-woocommerce' ),
						'options' => array(
							self::SYNC_MODE_SYNC_AND_SHOW => __( 'Sync and show in catalog', 'facebook-for-woocommerce' ),
							self::SYNC_MODE_SYNC_AND_HIDE => __( 'Sync and hide in catalog', 'facebook-for-woocommerce' ),
							self::SYNC_MODE_SYNC_DISABLED => __( 'Do not sync', 'facebook-for-woocommerce' ),
						),
						'value'   => $sync_mode,
					)
				);

				woocommerce_wp_textarea_input(
					array(
						'id'          => \WC_Facebookcommerce_Integration::FB_PRODUCT_DESCRIPTION,
						'label'       => __( 'Facebook Description', 'facebook-for-woocommerce' ),
						'desc_tip'    => true,
						'description' => __( 'Custom (plain-text only) description for product on Facebook. If blank, product description will be used. If product description is blank, shortname will be used.', 'facebook-for-woocommerce' ),
						'cols'        => 40,
						'rows'        => 20,
						'value'       => $description,
						'class'       => 'short enable-if-sync-enabled',
					)
				);

				woocommerce_wp_radio(
					array(
						'id'            => 'fb_product_image_source',
						'label'         => __( 'Facebook Product Image', 'facebook-for-woocommerce' ),
						'desc_tip'      => true,
						'description'   => __( 'Choose the product image that should be synced to the Facebook catalog for this product. If using a custom image, please enter an absolute URL (e.g. https://domain.com/image.jpg).', 'facebook-for-woocommerce' ),
						'options'       => array(
							Products::PRODUCT_IMAGE_SOURCE_PRODUCT => __( 'Use WooCommerce image', 'facebook-for-woocommerce' ),
							Products::PRODUCT_IMAGE_SOURCE_CUSTOM  => __( 'Use custom image', 'facebook-for-woocommerce' ),
						),
						'value'         => $image_source ?: Products::PRODUCT_IMAGE_SOURCE_PRODUCT,
						'class'         => 'short enable-if-sync-enabled js-fb-product-image-source',
						'wrapper_class' => 'fb-product-image-source-field',
					)
				);

				woocommerce_wp_text_input(
					array(
						'id'    => \WC_Facebook_Product::FB_PRODUCT_IMAGE,
						'label' => __( 'Custom Image URL', 'facebook-for-woocommerce' ),
						'value' => $image,
						'class' => sprintf( 'enable-if-sync-enabled product-image-source-field show-if-product-image-source-%s', Products::PRODUCT_IMAGE_SOURCE_CUSTOM ),
					)
				);

				woocommerce_wp_text_input(
					array(
						'id'          => \WC_Facebook_Product::FB_PRODUCT_PRICE,
						'label'       => sprintf(
						 /* translators: Placeholders %1$s - WC currency symbol */
							__( 'Facebook Price (%1$s)', 'facebook-for-woocommerce' ),
							get_woocommerce_currency_symbol()
						),
						'desc_tip'    => true,
						'description' => __( 'Custom price for product on Facebook. Please enter in monetary decimal (.) format without thousand separators and currency symbols. If blank, product price will be used.', 'facebook-for-woocommerce' ),
						'cols'        => 40,
						'rows'        => 60,
						'value'       => $price,
						'class'       => 'enable-if-sync-enabled',
					)
				);

				woocommerce_wp_hidden_input(
					array(
						'id'    => \WC_Facebook_Product::FB_REMOVE_FROM_SYNC,
						'value' => '',
					)
				);
				?>
			</div>
			<?php
			$commerce_handler = facebook_for_woocommerce()->get_commerce_handler();
			?>
			<?php if ( $commerce_handler->is_connected() && $commerce_handler->is_available() ) : ?>
				<div class='wc-facebook-commerce-options-group options_group'>
					<?php
					if ( $product instanceof \WC_Product ) {
						\WooCommerce\Facebook\Admin\Products::render_commerce_fields( $product );
					}
					?>
			</div>
			<?php endif; ?>
			<div class='wc-facebook-commerce-options-group options_group'>
				<?php \WooCommerce\Facebook\Admin\Products::render_google_product_category_fields_and_enhanced_attributes( $product ); ?>
			</div>
		</div>
		<?php
	}


	/**
	 * Outputs the Facebook settings fields for a single variation.
	 *
	 * @internal
	 *
	 * @since 1.10.0
	 *
	 * @param int      $index the index of the current variation
	 * @param array    $variation_data unused
	 * @param \WC_Post $post the post type for the current variation
	 */
	public function add_product_variation_edit_fields( $index, $variation_data, $post ) {

		$variation = wc_get_product( $post );

		if ( ! $variation instanceof \WC_Product_Variation ) {
			return;
		}

		$parent = wc_get_product( $variation->get_parent_id() );

		if ( ! $parent instanceof \WC_Product ) {
			return;
		}

		$sync_enabled = 'no' !== $this->get_product_variation_meta( $variation, Products::SYNC_ENABLED_META_KEY, $parent );
		$is_visible   = ( $visibility = $this->get_product_variation_meta( $variation, Products::VISIBILITY_META_KEY, $parent ) ) ? wc_string_to_bool( $visibility ) : true;

		$description  = $this->get_product_variation_meta( $variation, \WC_Facebookcommerce_Integration::FB_PRODUCT_DESCRIPTION, $parent );
		$price        = $this->get_product_variation_meta( $variation, \WC_Facebook_Product::FB_PRODUCT_PRICE, $parent );
		$image_url    = $this->get_product_variation_meta( $variation, \WC_Facebook_Product::FB_PRODUCT_IMAGE, $parent );
		$image_source = $variation->get_meta( Products::PRODUCT_IMAGE_SOURCE_META_KEY );

		if ( $sync_enabled ) {
			$sync_mode = $is_visible ? self::SYNC_MODE_SYNC_AND_SHOW : self::SYNC_MODE_SYNC_AND_HIDE;
		} else {
			$sync_mode = self::SYNC_MODE_SYNC_DISABLED;
		}

		woocommerce_wp_select(
			array(
				'id'            => "variable_facebook_sync_mode$index",
				'name'          => "variable_facebook_sync_mode[$index]",
				'label'         => __( 'Facebook sync', 'facebook-for-woocommerce' ),
				'options'       => array(
					self::SYNC_MODE_SYNC_AND_SHOW => __( 'Sync and show in catalog', 'facebook-for-woocommerce' ),
					self::SYNC_MODE_SYNC_AND_HIDE => __( 'Sync and hide in catalog', 'facebook-for-woocommerce' ),
					self::SYNC_MODE_SYNC_DISABLED => __( 'Do not sync', 'facebook-for-woocommerce' ),
				),
				'value'         => $sync_mode,
				'class'         => 'js-variable-fb-sync-toggle',
				'wrapper_class' => 'form-row form-row-full',
			)
		);

		woocommerce_wp_textarea_input(
			array(
				'id'            => sprintf( 'variable_%s%s', \WC_Facebookcommerce_Integration::FB_PRODUCT_DESCRIPTION, $index ),
				'name'          => sprintf( "variable_%s[$index]", \WC_Facebookcommerce_Integration::FB_PRODUCT_DESCRIPTION ),
				'label'         => __( 'Facebook Description', 'facebook-for-woocommerce' ),
				'desc_tip'      => true,
				'description'   => __( 'Custom (plain-text only) description for product on Facebook. If blank, product description will be used. If product description is blank, shortname will be used.', 'facebook-for-woocommerce' ),
				'cols'          => 40,
				'rows'          => 5,
				'value'         => $description,
				'class'         => 'enable-if-sync-enabled',
				'wrapper_class' => 'form-row form-row-full',
			)
		);

		woocommerce_wp_radio(
			array(
				'id'            => "variable_fb_product_image_source$index",
				'name'          => "variable_fb_product_image_source[$index]",
				'label'         => __( 'Facebook Product Image', 'facebook-for-woocommerce' ),
				'desc_tip'      => true,
				'description'   => __( 'Choose the product image that should be synced to the Facebook catalog for this product. If using a custom image, please enter an absolute URL (e.g. https://domain.com/image.jpg).', 'facebook-for-woocommerce' ),
				'options'       => array(
					Products::PRODUCT_IMAGE_SOURCE_PRODUCT        => __( 'Use variation image', 'facebook-for-woocommerce' ),
					Products::PRODUCT_IMAGE_SOURCE_PARENT_PRODUCT => __( 'Use parent image', 'facebook-for-woocommerce' ),
					Products::PRODUCT_IMAGE_SOURCE_CUSTOM         => __( 'Use custom image', 'facebook-for-woocommerce' ),
				),
				'value'         => $image_source ?: Products::PRODUCT_IMAGE_SOURCE_PRODUCT,
				'class'         => 'enable-if-sync-enabled js-fb-product-image-source',
				'wrapper_class' => 'fb-product-image-source-field',
			)
		);

		woocommerce_wp_text_input(
			array(
				'id'            => sprintf( 'variable_%s%s', \WC_Facebook_Product::FB_PRODUCT_IMAGE, $index ),
				'name'          => sprintf( "variable_%s[$index]", \WC_Facebook_Product::FB_PRODUCT_IMAGE ),
				'label'         => __( 'Custom Image URL', 'facebook-for-woocommerce' ),
				'value'         => $image_url,
				'class'         => sprintf( 'enable-if-sync-enabled product-image-source-field show-if-product-image-source-%s', Products::PRODUCT_IMAGE_SOURCE_CUSTOM ),
				'wrapper_class' => 'form-row form-row-full',
			)
		);

		woocommerce_wp_text_input(
			array(
				'id'            => sprintf( 'variable_%s%s', \WC_Facebook_Product::FB_PRODUCT_PRICE, $index ),
				'name'          => sprintf( "variable_%s[$index]", \WC_Facebook_Product::FB_PRODUCT_PRICE ),
				'label'         => sprintf(
				 /* translators: Placeholders %1$s - WC currency symbol */
					__( 'Facebook Price (%1$s)', 'facebook-for-woocommerce' ),
					get_woocommerce_currency_symbol()
				),
				'desc_tip'      => true,
				'description'   => __( 'Custom price for product on Facebook. Please enter in monetary decimal (.) format without thousand separators and currency symbols. If blank, product price will be used.', 'facebook-for-woocommerce' ),
				'value'         => wc_format_decimal( $price ),
				'class'         => 'enable-if-sync-enabled',
				'wrapper_class' => 'form-row form-full',
			)
		);
	}


	/**
	 * Gets the stored value for the given meta of a product variation.
	 *
	 * If no value is found, we try to use the value stored in the parent product.
	 *
	 * @since 1.10.0
	 *
	 * @param \WC_Product_Variation $variation the product variation
	 * @param string                $key the name of the meta to retrieve
	 * @param \WC_Product           $parent the parent product
	 * @return mixed
	 */
	private function get_product_variation_meta( $variation, $key, $parent ) {
		$value = $variation->get_meta( $key );
		if ( '' === $value && $parent instanceof \WC_Product ) {
			$value = $parent->get_meta( $key );
		}
		return $value;
	}


	/**
	 * Saves the submitted Facebook settings for each variation.
	 *
	 * @internal
	 *
	 * @since 1.10.0
	 *
	 * @param int $variation_id the ID of the product variation being edited
	 * @param int $index the index of the current variation
	 */
	public function save_product_variation_edit_fields( $variation_id, $index ) {
		$variation = wc_get_product( $variation_id );
		if ( ! $variation instanceof \WC_Product_Variation ) {
			return;
		}
		$sync_mode    = isset( $_POST['variable_facebook_sync_mode'][ $index ] ) ? wc_clean( wp_unslash( $_POST['variable_facebook_sync_mode'][ $index ] ) ) : self::SYNC_MODE_SYNC_DISABLED;
		$sync_enabled = self::SYNC_MODE_SYNC_DISABLED !== $sync_mode;
		if ( self::SYNC_MODE_SYNC_AND_SHOW === $sync_mode && $variation->is_virtual() ) {
			// force to Sync and hide
			$sync_mode = self::SYNC_MODE_SYNC_AND_HIDE;
		}
		// phpcs:disable WordPress.Security.NonceVerification.Missing
		if ( $sync_enabled ) {
			Products::enable_sync_for_products( array( $variation ) );
			Products::set_product_visibility( $variation, self::SYNC_MODE_SYNC_AND_HIDE !== $sync_mode );
			$posted_param = 'variable_' . \WC_Facebookcommerce_Integration::FB_PRODUCT_DESCRIPTION;
			$description  = isset( $_POST[ $posted_param ][ $index ] ) ? sanitize_text_field( wp_unslash( $_POST[ $posted_param ][ $index ] ) ) : null;
			$posted_param = 'variable_fb_product_image_source';
			$image_source = isset( $_POST[ $posted_param ][ $index ] ) ? sanitize_key( wp_unslash( $_POST[ $posted_param ][ $index ] ) ) : '';
			$posted_param = 'variable_' . \WC_Facebook_Product::FB_PRODUCT_IMAGE;
			$image_url    = isset( $_POST[ $posted_param ][ $index ] ) ? esc_url_raw( wp_unslash( $_POST[ $posted_param ][ $index ] ) ) : null;
			$posted_param = 'variable_' . \WC_Facebook_Product::FB_PRODUCT_PRICE;
			$price        = isset( $_POST[ $posted_param ][ $index ] ) ? wc_format_decimal( wc_clean( wp_unslash( $_POST[ $posted_param ][ $index ] ) ) ) : '';
			$variation->update_meta_data( \WC_Facebookcommerce_Integration::FB_PRODUCT_DESCRIPTION, $description );
			$variation->update_meta_data( Products::PRODUCT_IMAGE_SOURCE_META_KEY, $image_source );
			$variation->update_meta_data( \WC_Facebook_Product::FB_PRODUCT_IMAGE, $image_url );
			$variation->update_meta_data( \WC_Facebook_Product::FB_PRODUCT_PRICE, $price );
			$variation->save_meta_data();
		} else {
			Products::disable_sync_for_products( array( $variation ) );
		}//end if
		// phpcs:enable WordPress.Security.NonceVerification.Missing
	}


	/**
	 * Outputs a modal template in admin product pages.
	 *
	 * @internal
	 *
	 * @since 1.10.0
	 */
	public function render_modal_template() {
		global $current_screen;

		// bail if not on the products, product edit, or settings screen
		if ( ! $current_screen || ! in_array( $current_screen->id, $this->screen_ids, true ) ) {
			return;
		}
		?>
		<script type="text/template" id="tmpl-facebook-for-woocommerce-modal">
			<div class="wc-backbone-modal facebook-for-woocommerce-modal">
				<div class="wc-backbone-modal-content">
					<section class="wc-backbone-modal-main" role="main">
						<header class="wc-backbone-modal-header">
							<h1><?php esc_html_e( 'Facebook for WooCommerce', 'facebook-for-woocommerce' ); ?></h1>
							<button class="modal-close modal-close-link dashicons dashicons-no-alt">
								<span class="screen-reader-text"><?php esc_html_e( 'Close modal panel', 'facebook-for-woocommerce' ); ?></span>
							</button>
						</header>
						<article>{{{data.message}}}</article>
						<footer>
							<div class="inner">{{{data.buttons}}}</div>
						</footer>
					</section>
				</div>
			</div>
			<div class="wc-backbone-modal-backdrop modal-close"></div>
		</script>
		<?php
	}


}