255 lines
8.7 KiB
PHP
255 lines
8.7 KiB
PHP
<?php
|
|
/**
|
|
* Plugin Name: CommerceKit Floating Cart
|
|
* Plugin URI: https://www.commercegurus.com
|
|
* Description: Adds a floating cart icon (bottom-right) and auto-opens the CommerceKit minicart after add to cart. Requires CommerceGurus CommerceKit and WooCommerce.
|
|
* Version: 1.1.1
|
|
* Author: CommerceGurus
|
|
* Author URI: https://www.commercegurus.com
|
|
* License: GPLv3
|
|
* License URI: http://www.gnu.org/licenses/gpl.html
|
|
* Text Domain: cgkit-floating-cart
|
|
* Requires at least: 5.6
|
|
* Requires PHP: 7.4
|
|
* WC requires at least: 5.0
|
|
* WC tested up to: 9.0
|
|
*/
|
|
|
|
if ( ! defined( 'ABSPATH' ) ) {
|
|
exit;
|
|
}
|
|
|
|
define( 'CGKIT_FC_VERSION', '1.1.1' );
|
|
define( 'CGKIT_FC_PLUGIN_URL', plugin_dir_url( __FILE__ ) );
|
|
define( 'CGKIT_FC_PLUGIN_DIR', plugin_dir_path( __FILE__ ) );
|
|
|
|
/**
|
|
* Declare WooCommerce HPOS compatibility.
|
|
*/
|
|
add_action( 'before_woocommerce_init', function () {
|
|
if ( class_exists( \Automattic\WooCommerce\Utilities\FeaturesUtil::class ) ) {
|
|
\Automattic\WooCommerce\Utilities\FeaturesUtil::declare_compatibility( 'custom_order_tables', __FILE__, true );
|
|
}
|
|
} );
|
|
|
|
/**
|
|
* Force CommerceKit's "show FSN before shipping address is entered" flag on.
|
|
*
|
|
* Registered early (plugins_loaded priority 5) so it is in place before
|
|
* CommerceKit reads the option at plugins_loaded priority 10+.
|
|
*
|
|
* Why here rather than in the class: this filter must be attached BEFORE
|
|
* WooCommerce and CommerceKit finish loading so the very first option read
|
|
* during any request already sees fsn_before_ship = 1.
|
|
*
|
|
* What fsn_before_ship does inside CommerceKit:
|
|
* if ( ! $fsn_before_ship ) {
|
|
* if ( ! $show_shipping ) { return ''; } // bails for guests
|
|
* }
|
|
* Setting it to 1 skips that entire block, so the notification renders
|
|
* regardless of whether WooCommerce has calculated shipping yet.
|
|
*/
|
|
add_filter( 'option_commercekit', function ( $options ) {
|
|
if ( is_array( $options ) ) {
|
|
$options['fsn_before_ship'] = 1;
|
|
}
|
|
return $options;
|
|
}, 1 );
|
|
|
|
/**
|
|
* Fallback free-shipping amount for guests / users without a shipping address.
|
|
*
|
|
* CommerceKit bails with `if (!$free_amount) return ''` BEFORE it reaches the
|
|
* `fsn_before_ship` check. The bail happens because for guests WooCommerce zone
|
|
* detection falls back to "Rest of World" (empty destination in session), which
|
|
* often has no free_shipping method → amount stays 0.
|
|
*
|
|
* Fix: if the amount is still 0 after CommerceKit's own lookup, redo the zone
|
|
* detection using the shop's base country/state so we always resolve against
|
|
* the same zone as a logged-in customer in the store's home country.
|
|
*/
|
|
add_filter( 'commercekit_fsn_get_cart_options', function ( array $options ): array {
|
|
// Amount already resolved — nothing to do.
|
|
if ( $options['amount'] > 0 ) {
|
|
return $options;
|
|
}
|
|
|
|
// Re-run zone detection with the store base location.
|
|
$fake_package = [
|
|
'destination' => [
|
|
'country' => WC()->countries->get_base_country(),
|
|
'state' => WC()->countries->get_base_state(),
|
|
'postcode' => '',
|
|
'city' => '',
|
|
],
|
|
];
|
|
|
|
$zone = wc_get_shipping_zone( $fake_package );
|
|
$th_sep = wc_get_price_thousand_separator();
|
|
$dc_sep = wc_get_price_decimal_separator();
|
|
|
|
foreach ( $zone->get_shipping_methods( true ) as $method ) {
|
|
if ( 'free_shipping' !== $method->id ) {
|
|
continue;
|
|
}
|
|
|
|
$instance = $method->instance_settings ?? [];
|
|
$amount = isset( $instance['min_amount'] ) ? $instance['min_amount'] : 0;
|
|
$requires = isset( $instance['requires'] ) ? $instance['requires'] : '';
|
|
$amount = floatval( str_replace( $dc_sep, '.', str_replace( $th_sep, '', $amount ) ) );
|
|
|
|
// Only use this fallback when the method just needs a minimum order
|
|
// amount — coupon-gated thresholds can't be shown without a coupon.
|
|
if ( $amount > 0 && 'min_amount' === $requires ) {
|
|
$options['amount'] = $amount;
|
|
$options['requires'] = $requires;
|
|
$options['shipping'] = true;
|
|
}
|
|
break;
|
|
}
|
|
|
|
return $options;
|
|
}, 10 );
|
|
|
|
/**
|
|
* Main plugin class.
|
|
*/
|
|
class CommerceKit_Floating_Cart {
|
|
|
|
private static $instance = null;
|
|
|
|
public static function get_instance(): self {
|
|
if ( null === self::$instance ) {
|
|
self::$instance = new self();
|
|
}
|
|
return self::$instance;
|
|
}
|
|
|
|
private function __construct() {
|
|
add_action( 'wp_enqueue_scripts', [ $this, 'enqueue_assets' ] );
|
|
add_action( 'wp_footer', [ $this, 'render_floating_cart' ] );
|
|
add_filter( 'woocommerce_add_to_cart_fragments', [ $this, 'cart_count_fragment' ] );
|
|
|
|
// Disable WooCommerce's "redirect to cart page after add" so the page
|
|
// stays put and we can open the minicart instead.
|
|
add_filter( 'pre_option_woocommerce_cart_redirect_after_add', [ $this, 'disable_cart_redirect' ] );
|
|
|
|
// Ensure WooCommerce's AJAX add-to-cart is active so archive/shop page
|
|
// buttons fire the `added_to_cart` JS event without a page reload.
|
|
add_filter( 'pre_option_woocommerce_enable_ajax_add_to_cart', [ $this, 'enable_ajax_add_to_cart' ] );
|
|
}
|
|
|
|
/**
|
|
* Return 'no' so WooCommerce never redirects to /cart after adding a product.
|
|
*/
|
|
public function disable_cart_redirect( string $value ): string {
|
|
return 'no';
|
|
}
|
|
|
|
/**
|
|
* Return 'yes' so WooCommerce uses AJAX add-to-cart on archive/shop pages.
|
|
*/
|
|
public function enable_ajax_add_to_cart( string $value ): string {
|
|
return ( 'yes' === $value ) ? $value : 'yes';
|
|
}
|
|
|
|
/**
|
|
* Enqueue frontend CSS and JS.
|
|
*/
|
|
public function enqueue_assets(): void {
|
|
wp_enqueue_style(
|
|
'cgkit-floating-cart',
|
|
CGKIT_FC_PLUGIN_URL . 'assets/css/floating-cart.css',
|
|
[],
|
|
CGKIT_FC_VERSION
|
|
);
|
|
|
|
wp_enqueue_script(
|
|
'cgkit-floating-cart',
|
|
CGKIT_FC_PLUGIN_URL . 'assets/js/floating-cart.js',
|
|
[ 'jquery' ],
|
|
CGKIT_FC_VERSION,
|
|
true
|
|
);
|
|
|
|
/**
|
|
* Filter: cgkit_fc_minicart_trigger
|
|
* CSS selector(s) for the theme's minicart open button.
|
|
*/
|
|
$trigger = apply_filters(
|
|
'cgkit_fc_minicart_trigger',
|
|
'.shoptimizer-cart .cart-contents, .site-header-cart .cart-contents, .wcmenucart, .header-cart-link'
|
|
);
|
|
|
|
/** Filter: cgkit_fc_auto_open — set false to disable auto-open after add. */
|
|
$auto_open = (bool) apply_filters( 'cgkit_fc_auto_open', true );
|
|
|
|
/** Filter: cgkit_fc_auto_open_delay — ms to wait before opening (default 400). */
|
|
$delay = (int) apply_filters( 'cgkit_fc_auto_open_delay', 400 );
|
|
|
|
wp_localize_script( 'cgkit-floating-cart', 'cgkitFC', [
|
|
'minicartTrigger' => $trigger,
|
|
'autoOpen' => $auto_open ? 'yes' : 'no',
|
|
'autoOpenDelay' => $delay,
|
|
'cartUrl' => wc_get_cart_url(),
|
|
// WooCommerce AJAX endpoint used for single-product AJAX add-to-cart.
|
|
'wcAjaxUrl' => WC_AJAX::get_endpoint( '%%endpoint%%' ),
|
|
] );
|
|
}
|
|
|
|
/**
|
|
* Output the floating cart button HTML in the footer.
|
|
*/
|
|
public function render_floating_cart(): void {
|
|
$count = WC()->cart ? WC()->cart->get_cart_contents_count() : 0;
|
|
?>
|
|
<div id="cgkit-floating-cart" class="cgkit-floating-cart" role="complementary" aria-label="<?php esc_attr_e( 'Floating cart', 'cgkit-floating-cart' ); ?>">
|
|
<button class="cgkit-floating-cart__btn" type="button" aria-label="<?php esc_attr_e( 'Open mini cart', 'cgkit-floating-cart' ); ?>">
|
|
<?php echo $this->cart_icon_svg(); // phpcs:ignore WordPress.Security.EscapeOutput ?>
|
|
<?php echo $this->count_badge_html( $count ); // phpcs:ignore WordPress.Security.EscapeOutput ?>
|
|
</button>
|
|
</div>
|
|
<?php
|
|
}
|
|
|
|
/**
|
|
* WooCommerce fragment — keeps the badge count live after AJAX operations.
|
|
*/
|
|
public function cart_count_fragment( array $fragments ): array {
|
|
$count = WC()->cart ? WC()->cart->get_cart_contents_count() : 0;
|
|
$fragments['#cgkit-fc-count'] = $this->count_badge_html( $count );
|
|
return $fragments;
|
|
}
|
|
|
|
// -------------------------------------------------------------------------
|
|
// Private helpers
|
|
// -------------------------------------------------------------------------
|
|
|
|
private function count_badge_html( int $count ): string {
|
|
$hidden = 0 === $count ? ' cgkit-floating-cart__count--hidden' : '';
|
|
return sprintf(
|
|
'<span id="cgkit-fc-count" class="cgkit-floating-cart__count%s" aria-hidden="true">%s</span>',
|
|
esc_attr( $hidden ),
|
|
esc_html( $count > 0 ? $count : '' )
|
|
);
|
|
}
|
|
|
|
private function cart_icon_svg(): string {
|
|
return '<svg class="cgkit-floating-cart__icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"
|
|
width="24" height="24" aria-hidden="true" focusable="false"
|
|
fill="none" stroke="currentColor" stroke-width="2"
|
|
stroke-linecap="round" stroke-linejoin="round">
|
|
<circle cx="9" cy="21" r="1"></circle>
|
|
<circle cx="20" cy="21" r="1"></circle>
|
|
<path d="M1 1h4l2.68 13.39a2 2 0 0 0 2 1.61h9.72a2 2 0 0 0 2-1.61L23 6H6"></path>
|
|
</svg>';
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Bootstrap after WooCommerce is loaded so WC() is available.
|
|
*/
|
|
add_action( 'woocommerce_loaded', function () {
|
|
CommerceKit_Floating_Cart::get_instance();
|
|
} );
|