<?php
/**
* WooCommerce Square
*
* This source file is subject to the GNU General Public License v3.0
* that is bundled with this package in the file license.txt.
* It is also available through the world-wide-web at this URL:
* http://www.gnu.org/licenses/gpl-3.0.html GNU General Public License v3.0 or later
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@woocommerce.com so we can send you a copy immediately.
*
* DISCLAIMER
*
* Do not edit or add to this file if you wish to upgrade WooCommerce Square to newer
* versions in the future. If you wish to customize WooCommerce Square for your
* needs please refer to https://docs.woocommerce.com/document/woocommerce-square/
*
* @author WooCommerce
* @copyright Copyright: (c) 2019, Automattic, Inc.
* @license http://www.gnu.org/licenses/gpl-3.0.html GNU General Public License v3.0 or later
*/
namespace WooCommerce\Square;
defined( 'ABSPATH' ) || exit;
use WooCommerce\Square\Admin\Analytics\Revenue;
use WooCommerce\Square\Handlers\Products;
use WooCommerce\Square\Handlers\Product;
/**
* The base admin handler class.
*
* @since 2.0.0
*/
class Admin {
/**
* Product handler.
*
* @var Handlers\Products
*/
private $products_handler;
/**
* Privacy handler.
*
* @var Admin\Privacy
*/
private $privacy_handler;
/**
* Plugin
*
* @var Plugin plugin instance
*/
private $plugin;
/**
* Constructs the class.
*
* @since 2.0.0
*
* @param Plugin $plugin plugin instance.
*/
public function __construct( Plugin $plugin ) {
$this->plugin = $plugin;
$this->products_handler = $this->plugin->get_products_handler();
// privacy
$this->privacy_handler = new Admin\Privacy();
new Revenue();
$this->add_hooks();
}
/**
* Adds the action & filter hooks.
*
* @since 2.0.0
*/
private function add_hooks() {
// add the settings page.
add_filter(
'woocommerce_get_settings_pages',
function ( $pages ) {
$pages[] = new Admin\Settings_Page( $this->get_plugin()->get_settings_handler() );
return $pages;
}
);
// load admin scripts.
add_action(
'admin_enqueue_scripts',
function ( $hook ) {
$this->load_scripts_styles( $hook );
}
);
// Show warning notice for the user to manually trigger a sync if it has been a while since the last successful sync.
add_action( 'admin_notices', array( $this, 'maybe_show_sync_status_notice' ) );
// Show a notice when a stalled sync job was automatically recovered, prompting a re-run.
add_action( 'admin_notices', array( $this, 'maybe_show_sync_recovered_notice' ) );
}
/**
* Loads and enqueues admin scripts and styles.
*
* @param string $hook The current admin page.
*
* @since 2.0.0
*/
private function load_scripts_styles( $hook ) {
global $typenow;
if ( 'product' === $typenow ) {
wp_enqueue_script(
'wc-square-admin-products',
$this->get_plugin()->get_plugin_url() . '/build/assets/admin/wc-square-admin-products.js',
array( 'jquery' ),
Plugin::VERSION,
true
);
wp_enqueue_style(
'wc-square-admin-products',
$this->get_plugin()->get_plugin_url() . '/build/assets/admin/wc-square-admin-products-styles.css',
array(),
Plugin::VERSION
);
wp_localize_script(
'wc-square-admin-products',
'wc_square_admin_products',
array(
'ajax_url' => admin_url( 'admin-ajax.php' ),
'settings_url' => esc_url( $this->get_plugin()->get_settings_url() ),
'variable_product_types' => $this->get_variable_product_types(),
'synced_with_square_taxonomy' => Product::SYNCED_WITH_SQUARE_TAXONOMY,
'is_product_sync_enabled' => $this->get_plugin()->get_settings_handler()->is_product_sync_enabled(),
'is_woocommerce_sor' => $this->get_plugin()->get_settings_handler()->is_system_of_record_woocommerce(),
'is_square_sor' => $this->get_plugin()->get_settings_handler()->is_system_of_record_square(),
'is_inventory_sync_enabled' => $this->get_plugin()->get_settings_handler()->is_inventory_sync_enabled(),
'get_quick_edit_product_details_nonce' => wp_create_nonce( 'get-quick-edit-product-details' ),
'fetch_product_stock_with_square_nonce' => wp_create_nonce( 'fetch-product-stock-with-square' ),
'supported_products_for_sync' => array( 'simple', 'variable' ),
'i18n' => array(
'inventory_tracking_disabled' => __( 'Inventory tracking is disabled for this product', 'woocommerce-square' ),
'synced_with_square' => __( 'Synced with Square', 'woocommerce-square' ),
'managed_by_square' => __( 'Managed by Square', 'woocommerce-square' ),
'fetch_stock_with_square' => __( 'Fetch stock from Square', 'woocommerce-square' ),
'sync_inventory' => __( 'Sync inventory', 'woocommerce-square' ),
'sync_stock_from_square' => __( 'Sync stock from Square', 'woocommerce-square' ),
'attribute_name_too_long' => __( 'Attribute name is too long, maximum allowed are 65 characters', 'woocommerce-square' ),
'too_many_attributes' => __( 'Too many attributes, maximum allowed are 6.', 'woocommerce-square' ),
/* translators: %d - maximum allowed attribute values */
'too_many_attribute_values' => __( 'Too many attribute values: %d (max 250)', 'woocommerce-square' ),
'too_many_variations' => __( 'Too many variations, maximum allowed are 250.', 'woocommerce-square' ),
),
)
);
} elseif ( $this->get_plugin()->is_plugin_settings() ) {
wp_enqueue_style(
'wc-square-admin',
$this->get_plugin()->get_plugin_url() . '/build/assets/admin/wc-square-admin.css',
array(),
Plugin::VERSION
);
wp_enqueue_media();
wp_enqueue_script(
'wc-square-admin-settings',
$this->get_plugin()->get_plugin_url() . '/build/assets/admin/wc-square-admin-settings.js',
array( 'jquery', 'jquery-blockui', 'backbone', 'wc-backbone-modal' ),
Plugin::VERSION,
true
);
$sync_job = $this->get_plugin()->get_sync_handler()->get_job_in_progress();
if ( $sync_job ) {
$existing_sync_id = $sync_job->id;
} else {
$existing_sync_id = false;
}
wp_localize_script(
'wc-square-admin-settings',
'wc_square_admin_settings',
array(
'ajax_url' => admin_url( 'admin-ajax.php' ),
'is_product_sync_enabled' => $this->get_plugin()->get_settings_handler()->is_product_sync_enabled(),
'is_woocommerce_sor' => $this->get_plugin()->get_settings_handler()->is_system_of_record_woocommerce(),
'is_square_sor' => $this->get_plugin()->get_settings_handler()->is_system_of_record_square(),
'is_inventory_sync_enabled' => $this->get_plugin()->get_settings_handler()->is_inventory_sync_enabled(),
'is_sandbox' => $this->get_plugin()->get_settings_handler()->is_sandbox(),
'existing_sync_job_id' => $existing_sync_id,
'import_products_from_square' => wp_create_nonce( 'import-products-from-square' ),
'sync_products_with_square' => wp_create_nonce( 'sync-products-with-square' ),
'get_sync_with_square_status_nonce' => wp_create_nonce( 'get-sync-with-square-status' ),
'handle_sync_with_square_records' => wp_create_nonce( 'handle-sync-with-square-records' ),
'i18n' => array(
'resolved' => __( 'Resolved', 'woocommerce-square' ),
'no_records_found' => __( 'No records found', 'woocommerce-square' ),
'skipped' => __( 'Skipped', 'woocommerce-square' ),
'updated' => __( 'Updated', 'woocommerce-square' ),
'imported' => __( 'Imported', 'woocommerce-square' ),
'sync_inventory_label' => array(
'square' => __( 'Enable to fetch inventory changes from Square', 'woocommerce-square' ),
'woocommerce' => __( 'Enable to push inventory changes to Square', 'woocommerce-square' ),
),
'sync_inventory_description' => array(
'square' => __( 'Inventory is fetched from Square periodically and updated in WooCommerce', 'woocommerce-square' ),
'woocommerce' => sprintf(
/* translators: Placeholders: %1$s - <strong> tag, %2$s - </strong> tag */
__( 'Inventory is %1$salways fetched from Square%2$s periodically to account for sales from other channels.', 'woocommerce-square' ),
'<strong>',
'</strong>'
),
),
),
)
);
$asset_file = WC_SQUARE_PLUGIN_PATH . 'build/settings.asset.php';
if ( ! file_exists( $asset_file ) ) {
return;
}
$asset = include $asset_file;
wp_enqueue_script(
'woocommerce-square-settings-js',
WC_SQUARE_PLUGIN_URL . 'build/settings.js',
$asset['dependencies'],
$asset['version'],
array(
'in_footer' => true,
)
);
$gift_card_placeholder_url = Products::get_gift_card_default_placeholder_url();
if ( empty( $gift_card_placeholder_url ) ) {
$gift_card_placeholder_url = WC_SQUARE_PLUGIN_URL . 'build/images/gift-card-featured-image.png';
}
wp_localize_script(
'woocommerce-square-settings-js',
'wcSquareSettings',
array(
'nonce' => wp_create_nonce( 'wc_square_settings' ),
'homeUrl' => home_url(),
'adminUrl' => admin_url(),
'ajaxUrl' => admin_url( 'admin-ajax.php' ),
'depsCheck' => $this->get_plugin()->get_dependency_handler()->meets_php_dependencies(),
'gcPlaceholderUrl' => esc_url( $gift_card_placeholder_url ),
)
);
wp_enqueue_style(
'woocommerce-square-settings-css',
WC_SQUARE_PLUGIN_URL . 'build/settings.css',
array(),
$asset['version'],
);
} elseif ( 'woocommerce_page_woocommerce-square-onboarding' === $hook ) {
$asset_file = WC_SQUARE_PLUGIN_PATH . 'build/onboarding.asset.php';
if ( ! file_exists( $asset_file ) ) {
return;
}
$asset = include $asset_file;
wp_enqueue_script(
'woocommerce-square-onboarding-js',
WC_SQUARE_PLUGIN_URL . 'build/onboarding.js',
$asset['dependencies'],
$asset['version'],
array(
'in_footer' => true,
)
);
wp_localize_script(
'woocommerce-square-onboarding-js',
'wcSquareSettings',
array(
'nonce' => wp_create_nonce( 'wc_square_settings' ),
'homeUrl' => home_url(),
'adminUrl' => admin_url(),
'ajaxUrl' => admin_url( 'admin-ajax.php' ),
)
);
wp_enqueue_style(
'woocommerce-square-onboarding-css',
WC_SQUARE_PLUGIN_URL . 'build/onboarding.css',
array(),
$asset['version'],
);
wp_localize_script(
'woocommerce-square-onboarding-js',
'wcSquareOnboarding',
array(
'plugin_version' => WC_SQUARE_PLUGIN_VERSION,
'is_mobile' => wp_is_mobile(),
)
);
}
wp_enqueue_style( 'wp-components' );
}
/**
* Gets a list of variable product types.
*
* @since 2.0.0
*
* @return string[]
*/
private function get_variable_product_types() {
/**
* Filters the variable product types.
*
* @since 2.0.0
*
* @param string[] array of product types
*/
return (array) apply_filters( 'wc_square_variable_product_types', array( 'variable', 'variable-subscription' ) );
}
/**
* Gets the products handler.
*
* @since 2.0.0
*
* @return Admin\Products
*/
public function get_products_handler() {
return $this->products_handler;
}
/**
* Gets the plugin instance.
*
* @since 2.0.0
*
* @return Plugin
*/
protected function get_plugin() {
return $this->plugin;
}
/**
* Maybe show a warning notice for the user to manually trigger a sync if it has been a while since the last successful sync.
*
* If the following conditions are met, show the notice:
* - The connection is established.
* - The location is set.
* - The product sync is enabled.
* - The last synced at is set and is before the threshold time (default is 24 hours, 48 hours if the sync interval is 24 hours).
* - The synced products count is greater than 0 (at least one product is synced with Square).
*
* @since 5.3.3
*
* @return void
*/
public function maybe_show_sync_status_notice() {
// Bail if the user does not have the manage_woocommerce capability.
if ( ! current_user_can( 'manage_woocommerce' ) ) { // phpcs:ignore WordPress.WP.Capabilities.Unknown
return;
}
// Bail if the connection is not established, the location is not set, or the product sync is not enabled.
$is_connected = wc_square()->get_settings_handler()->is_connected();
$location_id = wc_square()->get_settings_handler()->get_location_id();
$is_product_sync_enabled = wc_square()->get_settings_handler()->is_product_sync_enabled();
if ( ! $is_connected || ! $location_id || ! $is_product_sync_enabled ) {
return;
}
// Bail if the last synced at is not set.
$last_synced_at = wc_square()->get_sync_handler()->get_last_synced_at();
if ( ! $last_synced_at ) {
return;
}
// The threshold time is 24 hours by default, 48 hours if the sync interval is 24 hours.
$sync_interval = wc_square()->get_settings_handler()->get_sync_interval();
$threshold_seconds = $sync_interval >= 24 * HOUR_IN_SECONDS ? 48 * HOUR_IN_SECONDS : 24 * HOUR_IN_SECONDS;
/**
* Filters the threshold time for the sync status notice.
*
* @since 5.3.3
*
* @param int $threshold_seconds The threshold time in seconds. Default is 24 hours, or 48 hours when the sync interval is 24 hours.
* @param int $sync_interval The sync interval in seconds.
*/
$threshold_seconds = apply_filters( 'wc_square_sync_status_notice_threshold_seconds', $threshold_seconds, $sync_interval );
$threshold_time = time() - $threshold_seconds;
// Bail if the last synced at is after the threshold time.
if ( $last_synced_at > $threshold_time ) {
return;
}
// Get the synced products count.
$synced_products_count_key = 'wc_square_synced_products_count_' . $location_id;
$synced_products_count = get_transient( $synced_products_count_key );
if ( false === $synced_products_count ) {
$synced_products_count = count( Product::get_products_synced_with_square() );
// Set the transient for the synced products count.
set_transient( $synced_products_count_key, $synced_products_count, $threshold_seconds );
}
// Bail if synced products count is 0.
if ( 0 === (int) $synced_products_count ) {
return;
}
// Show the notice if the last synced at is before the threshold time.
?>
<div class="notice notice-warning is-dismissible">
<p>
<?php
echo wp_kses(
sprintf(
/* translators: Placeholders: %1$s - <a> tag, %2$s - </a> tag, %3$s - <a> tag, %4$s - </a> tag */
__( 'It has been a while since the last successful sync. Please manually trigger a sync from the %1$supdate page%2$s, or clear the ongoing sync from the %3$stools page%4$s if it is stuck, to ensure that your products are up to date.', 'woocommerce-square' ),
'<a href="' . esc_url( admin_url( 'admin.php?page=wc-settings&tab=square§ion=update' ) ) . '">',
'</a>',
'<a href="' . esc_url( admin_url( 'admin.php?page=wc-status&tab=tools' ) ) . '">',
'</a>'
),
array(
'a' => array(
'href' => array(),
),
)
);
?>
</p>
</div>
<?php
}
/**
* Maybe show a notice that a stalled sync job was automatically recovered.
*
* Shown when auto-recovery has fired and no successful sync has completed since, so the
* merchant knows a stuck sync was stopped and can start a new one.
* Clears itself once a sync completes successfully after the recovery.
*
* @since 5.5.0
*
* @return void
*/
public function maybe_show_sync_recovered_notice() {
if ( ! current_user_can( 'manage_woocommerce' ) ) { // phpcs:ignore WordPress.WP.Capabilities.Unknown
return;
}
$settings = wc_square()->get_settings_handler();
if ( ! $settings->is_connected() || ! $settings->get_location_id() || ! $settings->is_product_sync_enabled() ) {
return;
}
$recovered_at = (int) get_option( 'wc_square_sync_auto_recovered_at', 0 );
if ( ! $recovered_at ) {
return;
}
// The flag itself is the only signal used here. It is cleared when a sync the merchant started
// begins, and again when such a job completes, so there is nothing left to infer.
//
// It deliberately does NOT compare against the last synced timestamp: interval polling advances
// that every few minutes, so any comparison against it would dismiss this notice long before
// the merchant saw it, which is the same reason polls are excluded from both clearing paths.
?>
<div class="notice notice-warning is-dismissible">
<p>
<?php
echo wp_kses(
sprintf(
/* translators: Placeholders: %1$s - <a> tag, %2$s - </a> tag */
__( 'A stuck Square sync job was detected and automatically stopped. Product data may be out of date, please start a new sync from the %1$supdate page%2$s.', 'woocommerce-square' ),
'<a href="' . esc_url( admin_url( 'admin.php?page=wc-settings&tab=square§ion=update' ) ) . '">',
'</a>'
),
array(
'a' => array(
'href' => array(),
),
)
);
?>
</p>
</div>
<?php
}
}