| 1 |
<?php |
| 2 |
/** |
| 3 |
* Free Shipping Progress Bar. |
| 4 |
* |
| 5 |
* Shows how much a customer still has to add to qualify for free shipping. |
| 6 |
* Reads the threshold from the store's own WooCommerce shipping zones, so it |
| 7 |
* keeps working when the merchant changes the rule. |
| 8 |
* |
| 9 |
* @package King_Addons |
| 10 |
*/ |
| 11 |
|
| 12 |
namespace King_Addons; |
| 13 |
|
| 14 |
if (!defined('ABSPATH')) { |
| 15 |
exit; |
| 16 |
} |
| 17 |
|
| 18 |
/** |
| 19 |
* Free shipping progress bar module. |
| 20 |
*/ |
| 21 |
final class Free_Shipping_Bar |
| 22 |
{ |
| 23 |
/** |
| 24 |
* Settings option key. |
| 25 |
*/ |
| 26 |
public const OPTION_KEY = 'king_addons_free_shipping_bar_settings'; |
| 27 |
|
| 28 |
/** |
| 29 |
* Singleton instance. |
| 30 |
* |
| 31 |
* @var Free_Shipping_Bar|null |
| 32 |
*/ |
| 33 |
private static ?Free_Shipping_Bar $instance = null; |
| 34 |
|
| 35 |
/** |
| 36 |
* Cached settings. |
| 37 |
* |
| 38 |
* @var array<string, mixed> |
| 39 |
*/ |
| 40 |
private array $settings = []; |
| 41 |
|
| 42 |
/** |
| 43 |
* Guards against printing the bar twice on one request. |
| 44 |
* |
| 45 |
* @var bool |
| 46 |
*/ |
| 47 |
private bool $printed = false; |
| 48 |
|
| 49 |
/** |
| 50 |
* Singleton accessor. |
| 51 |
* |
| 52 |
* @return Free_Shipping_Bar |
| 53 |
*/ |
| 54 |
public static function instance(): Free_Shipping_Bar |
| 55 |
{ |
| 56 |
if (is_null(self::$instance)) { |
| 57 |
self::$instance = new self(); |
| 58 |
} |
| 59 |
|
| 60 |
return self::$instance; |
| 61 |
} |
| 62 |
|
| 63 |
/** |
| 64 |
* Hooks. |
| 65 |
*/ |
| 66 |
public function __construct() |
| 67 |
{ |
| 68 |
$this->settings = $this->get_settings(); |
| 69 |
|
| 70 |
add_action('admin_menu', [$this, 'register_admin_menu'], 15); |
| 71 |
add_action('admin_post_king_addons_fsb_save', [$this, 'handle_save']); |
| 72 |
|
| 73 |
add_action('wp_enqueue_scripts', [$this, 'maybe_enqueue_assets']); |
| 74 |
|
| 75 |
// Classic (shortcode) cart and checkout. |
| 76 |
add_action('woocommerce_before_cart', [$this, 'render_for_cart'], 5); |
| 77 |
add_action('woocommerce_before_checkout_form', [$this, 'render_for_checkout'], 5); |
| 78 |
|
| 79 |
// Block cart and checkout are the default for new stores. They fire |
| 80 |
// neither the classic hooks nor the_content, so hook the blocks directly. |
| 81 |
add_filter('render_block_woocommerce/cart', [$this, 'prepend_to_cart_block'], 10, 1); |
| 82 |
add_filter('render_block_woocommerce/checkout', [$this, 'prepend_to_checkout_block'], 10, 1); |
| 83 |
|
| 84 |
// Block-theme mini-cart never fires woocommerce_before_mini_cart. |
| 85 |
add_filter('render_block_woocommerce/filled-mini-cart-contents-block', [$this, 'prepend_to_mini_cart_block'], 10, 1); |
| 86 |
|
| 87 |
// Woo Builder cart/checkout templates skip the classic Woo hooks. |
| 88 |
add_action('king_addons/woo_builder/before_render', [$this, 'render_for_woo_builder'], 5, 2); |
| 89 |
|
| 90 |
// Mini-cart placement (also refreshed by WooCommerce fragments). |
| 91 |
add_action('woocommerce_before_mini_cart', [$this, 'render_for_mini_cart'], 5); |
| 92 |
|
| 93 |
// Keep the bar current after AJAX cart updates. |
| 94 |
add_filter('woocommerce_add_to_cart_fragments', [$this, 'add_cart_fragment']); |
| 95 |
add_action('wp_ajax_king_addons_fsb_markup', [$this, 'ajax_markup']); |
| 96 |
add_action('wp_ajax_nopriv_king_addons_fsb_markup', [$this, 'ajax_markup']); |
| 97 |
} |
| 98 |
|
| 99 |
/** |
| 100 |
* Default settings. |
| 101 |
* |
| 102 |
* @return array<string, mixed> |
| 103 |
*/ |
| 104 |
public function get_default_settings(): array |
| 105 |
{ |
| 106 |
return [ |
| 107 |
'enabled' => false, |
| 108 |
'threshold_source' => 'auto', |
| 109 |
'manual_amount' => '', |
| 110 |
'show_on_cart' => 'yes', |
| 111 |
'show_on_checkout' => 'yes', |
| 112 |
'show_in_mini_cart' => 'yes', |
| 113 |
'text_progress' => __('Add {remaining} more and your shipping is on us.', 'king-addons'), |
| 114 |
'text_success' => __('Free shipping unlocked.', 'king-addons'), |
| 115 |
'bar_height' => 8, |
| 116 |
'bar_color' => '#2f9e5f', |
| 117 |
'bar_track_color' => '#e6e6e6', |
| 118 |
'text_color' => '#1f2933', |
| 119 |
'hide_when_reached' => '', |
| 120 |
]; |
| 121 |
} |
| 122 |
|
| 123 |
/** |
| 124 |
* Settings with defaults applied. |
| 125 |
* |
| 126 |
* @return array<string, mixed> |
| 127 |
*/ |
| 128 |
public function get_settings(): array |
| 129 |
{ |
| 130 |
$saved = get_option(self::OPTION_KEY, []); |
| 131 |
if (!is_array($saved)) { |
| 132 |
$saved = []; |
| 133 |
} |
| 134 |
|
| 135 |
return array_merge($this->get_default_settings(), $saved); |
| 136 |
} |
| 137 |
|
| 138 |
/** |
| 139 |
* Whether the module should render at all. |
| 140 |
* |
| 141 |
* @return bool |
| 142 |
*/ |
| 143 |
private function is_active(): bool |
| 144 |
{ |
| 145 |
if (empty($this->settings['enabled'])) { |
| 146 |
return false; |
| 147 |
} |
| 148 |
|
| 149 |
// admin-ajax.php reports is_admin() === true, so treating every admin |
| 150 |
// request as "off" dropped the bar from cart-table AJAX fragments and |
| 151 |
// from woocommerce_add_to_cart_fragments. |
| 152 |
$front_or_ajax = !is_admin() || wp_doing_ajax(); |
| 153 |
|
| 154 |
return class_exists('WooCommerce') && $front_or_ajax; |
| 155 |
} |
| 156 |
|
| 157 |
/** |
| 158 |
* Free shipping threshold for the current customer, or null when the store |
| 159 |
* has no amount-based free shipping rule. |
| 160 |
* |
| 161 |
* @return float|null |
| 162 |
*/ |
| 163 |
public function get_threshold(): ?float |
| 164 |
{ |
| 165 |
if ('manual' === ($this->settings['threshold_source'] ?? 'auto')) { |
| 166 |
$amount = (float) ($this->settings['manual_amount'] ?? 0); |
| 167 |
|
| 168 |
return $amount > 0 ? $amount : null; |
| 169 |
} |
| 170 |
|
| 171 |
if (!function_exists('WC') || !class_exists('WC_Shipping_Zones')) { |
| 172 |
return null; |
| 173 |
} |
| 174 |
|
| 175 |
$package = ['destination' => $this->get_package_destination()]; |
| 176 |
if (WC()->cart && method_exists(WC()->cart, 'get_shipping_packages')) { |
| 177 |
$packages = WC()->cart->get_shipping_packages(); |
| 178 |
if (!empty($packages)) { |
| 179 |
$package = reset($packages); |
| 180 |
} |
| 181 |
} |
| 182 |
|
| 183 |
$zone = \WC_Shipping_Zones::get_zone_matching_package($package); |
| 184 |
if (!$zone) { |
| 185 |
return null; |
| 186 |
} |
| 187 |
|
| 188 |
$amounts = []; |
| 189 |
foreach ($zone->get_shipping_methods(true) as $method) { |
| 190 |
if ('free_shipping' !== $method->id) { |
| 191 |
continue; |
| 192 |
} |
| 193 |
|
| 194 |
$requires = $method->get_option('requires'); |
| 195 |
if (!in_array($requires, ['min_amount', 'either', 'both'], true)) { |
| 196 |
continue; |
| 197 |
} |
| 198 |
|
| 199 |
$amount = (float) $method->get_option('min_amount'); |
| 200 |
if ($amount > 0) { |
| 201 |
$amounts[] = $amount; |
| 202 |
} |
| 203 |
} |
| 204 |
|
| 205 |
if (empty($amounts)) { |
| 206 |
return null; |
| 207 |
} |
| 208 |
|
| 209 |
// A zone can carry several free shipping methods; the cheapest one is |
| 210 |
// the one the customer will actually hit first. |
| 211 |
return (float) min($amounts); |
| 212 |
} |
| 213 |
|
| 214 |
/** |
| 215 |
* A destination WooCommerce can match a shipping zone against. |
| 216 |
* |
| 217 |
* WC_Shipping_Zones::get_zone_matching_package() reads country, state and |
| 218 |
* postcode straight out of the array with no checks of its own, so handing |
| 219 |
* it an empty destination - which is what happens in wp-admin, where there |
| 220 |
* is no cart - printed six "Undefined array key" warnings over the settings |
| 221 |
* screen. |
| 222 |
* |
| 223 |
* @return array<string,string> |
| 224 |
*/ |
| 225 |
private function get_package_destination(): array |
| 226 |
{ |
| 227 |
$destination = [ |
| 228 |
'country' => '', |
| 229 |
'state' => '', |
| 230 |
'postcode' => '', |
| 231 |
'city' => '', |
| 232 |
'address' => '', |
| 233 |
'address_2' => '', |
| 234 |
]; |
| 235 |
|
| 236 |
$customer = function_exists('WC') ? WC()->customer : null; |
| 237 |
|
| 238 |
if ($customer) { |
| 239 |
$destination['country'] = (string) $customer->get_shipping_country(); |
| 240 |
$destination['state'] = (string) $customer->get_shipping_state(); |
| 241 |
$destination['postcode'] = (string) $customer->get_shipping_postcode(); |
| 242 |
$destination['city'] = (string) $customer->get_shipping_city(); |
| 243 |
} |
| 244 |
|
| 245 |
// No customer session, which is the normal case in wp-admin: fall back |
| 246 |
// to where the store itself is, so the detected threshold is the one |
| 247 |
// most customers will see. |
| 248 |
if ('' === $destination['country'] && function_exists('wc_get_base_location')) { |
| 249 |
$base = wc_get_base_location(); |
| 250 |
$destination['country'] = (string) ($base['country'] ?? ''); |
| 251 |
$destination['state'] = (string) ($base['state'] ?? ''); |
| 252 |
} |
| 253 |
|
| 254 |
return $destination; |
| 255 |
} |
| 256 |
|
| 257 |
/** |
| 258 |
* Cart total the threshold is measured against. |
| 259 |
* |
| 260 |
* @return float |
| 261 |
*/ |
| 262 |
private function get_cart_total(): float |
| 263 |
{ |
| 264 |
if (!function_exists('WC') || !WC()->cart) { |
| 265 |
return 0.0; |
| 266 |
} |
| 267 |
|
| 268 |
// Subtotal after discounts, excluding shipping - this is what |
| 269 |
// WooCommerce's own free shipping method compares. |
| 270 |
$total = (float) WC()->cart->get_displayed_subtotal(); |
| 271 |
|
| 272 |
if (WC()->cart->display_prices_including_tax()) { |
| 273 |
$total -= (float) WC()->cart->get_discount_tax(); |
| 274 |
} |
| 275 |
|
| 276 |
$total -= (float) WC()->cart->get_discount_total(); |
| 277 |
|
| 278 |
return max(0.0, $total); |
| 279 |
} |
| 280 |
|
| 281 |
/** |
| 282 |
* Build the bar markup, or an empty string when there is nothing to show. |
| 283 |
* |
| 284 |
* @param bool $mark_printed Whether this render should block a second cart/checkout copy. |
| 285 |
* |
| 286 |
* @return string |
| 287 |
*/ |
| 288 |
public function get_markup(bool $mark_printed = true): string |
| 289 |
{ |
| 290 |
if (!$this->is_active()) { |
| 291 |
return ''; |
| 292 |
} |
| 293 |
|
| 294 |
$threshold = $this->get_threshold(); |
| 295 |
if (null === $threshold || $threshold <= 0) { |
| 296 |
return ''; |
| 297 |
} |
| 298 |
|
| 299 |
$total = $this->get_cart_total(); |
| 300 |
$reached = $total >= $threshold; |
| 301 |
|
| 302 |
if ($reached && !empty($this->settings['hide_when_reached'])) { |
| 303 |
return ''; |
| 304 |
} |
| 305 |
|
| 306 |
$remaining = max(0.0, $threshold - $total); |
| 307 |
$percent = $threshold > 0 ? min(100, ($total / $threshold) * 100) : 0; |
| 308 |
|
| 309 |
$text = $reached |
| 310 |
? (string) $this->settings['text_success'] |
| 311 |
: str_replace( |
| 312 |
['{remaining}', '{total}', '{threshold}'], |
| 313 |
[wc_price($remaining), wc_price($total), wc_price($threshold)], |
| 314 |
(string) $this->settings['text_progress'] |
| 315 |
); |
| 316 |
|
| 317 |
$style = sprintf( |
| 318 |
'--ka-fsb-height:%dpx;--ka-fsb-color:%s;--ka-fsb-track:%s;--ka-fsb-text:%s;', |
| 319 |
(int) $this->settings['bar_height'], |
| 320 |
sanitize_hex_color((string) $this->settings['bar_color']) ?: '#2f9e5f', |
| 321 |
sanitize_hex_color((string) $this->settings['bar_track_color']) ?: '#e6e6e6', |
| 322 |
sanitize_hex_color((string) $this->settings['text_color']) ?: '#1f2933' |
| 323 |
); |
| 324 |
|
| 325 |
ob_start(); |
| 326 |
?> |
| 327 |
<div class="king-addons-fsb<?php echo $reached ? ' is-reached' : ''; ?>" style="<?php echo esc_attr($style); ?>"> |
| 328 |
<p class="king-addons-fsb__text"> |
| 329 |
<?php |
| 330 |
// wc_price() returns markup; the surrounding text is translator-supplied. |
| 331 |
echo wp_kses_post($text); |
| 332 |
?> |
| 333 |
</p> |
| 334 |
<div class="king-addons-fsb__track" role="progressbar" |
| 335 |
aria-valuemin="0" aria-valuemax="100" |
| 336 |
aria-valuenow="<?php echo esc_attr((string) round($percent)); ?>"> |
| 337 |
<span class="king-addons-fsb__fill" style="width:<?php echo esc_attr((string) round($percent, 2)); ?>%"></span> |
| 338 |
</div> |
| 339 |
</div> |
| 340 |
<?php |
| 341 |
|
| 342 |
if ($mark_printed) { |
| 343 |
$this->printed = true; |
| 344 |
} |
| 345 |
|
| 346 |
return (string) ob_get_clean(); |
| 347 |
} |
| 348 |
|
| 349 |
/** |
| 350 |
* Prepend the bar to the Cart block. |
| 351 |
* |
| 352 |
* @param string $block_content Rendered block markup. |
| 353 |
* |
| 354 |
* @return string |
| 355 |
*/ |
| 356 |
public function prepend_to_cart_block(string $block_content): string |
| 357 |
{ |
| 358 |
if (!$this->is_active() || $this->printed || empty($this->settings['show_on_cart'])) { |
| 359 |
return $block_content; |
| 360 |
} |
| 361 |
|
| 362 |
return $this->get_markup() . $block_content; |
| 363 |
} |
| 364 |
|
| 365 |
/** |
| 366 |
* Prepend the bar to the Checkout block. |
| 367 |
* |
| 368 |
* @param string $block_content Rendered block markup. |
| 369 |
* |
| 370 |
* @return string |
| 371 |
*/ |
| 372 |
public function prepend_to_checkout_block(string $block_content): string |
| 373 |
{ |
| 374 |
if (!$this->is_active() || $this->printed || empty($this->settings['show_on_checkout'])) { |
| 375 |
return $block_content; |
| 376 |
} |
| 377 |
|
| 378 |
return $this->get_markup() . $block_content; |
| 379 |
} |
| 380 |
|
| 381 |
/** |
| 382 |
* Prepend the bar to the filled Mini-Cart block (block themes). |
| 383 |
* |
| 384 |
* @param string $block_content Rendered block markup. |
| 385 |
* |
| 386 |
* @return string |
| 387 |
*/ |
| 388 |
public function prepend_to_mini_cart_block(string $block_content): string |
| 389 |
{ |
| 390 |
if (!$this->is_active() || empty($this->settings['show_in_mini_cart'])) { |
| 391 |
return $block_content; |
| 392 |
} |
| 393 |
|
| 394 |
$markup = $this->get_markup(false); |
| 395 |
if ('' === $markup) { |
| 396 |
return $block_content; |
| 397 |
} |
| 398 |
|
| 399 |
$injected = preg_replace('/(<div\b[^>]*>)/', '$1' . $markup, $block_content, 1); |
| 400 |
|
| 401 |
return is_string($injected) ? $injected : $markup . $block_content; |
| 402 |
} |
| 403 |
|
| 404 |
/** |
| 405 |
* Woo Builder cart/checkout placement. |
| 406 |
* |
| 407 |
* @param string $context Template context. |
| 408 |
* @param mixed $template_id Template id. |
| 409 |
* |
| 410 |
* @return void |
| 411 |
*/ |
| 412 |
public function render_for_woo_builder(string $context, $template_id): void |
| 413 |
{ |
| 414 |
unset($template_id); |
| 415 |
|
| 416 |
if ('cart' === $context) { |
| 417 |
$this->render_for_cart(); |
| 418 |
return; |
| 419 |
} |
| 420 |
|
| 421 |
if ('checkout' === $context) { |
| 422 |
$this->render_for_checkout(); |
| 423 |
} |
| 424 |
} |
| 425 |
|
| 426 |
/** |
| 427 |
* Fresh markup for JS refreshers (block mini-cart quantity changes). |
| 428 |
* |
| 429 |
* @return void |
| 430 |
*/ |
| 431 |
public function ajax_markup(): void |
| 432 |
{ |
| 433 |
check_ajax_referer('king_addons_fsb', 'nonce'); |
| 434 |
|
| 435 |
$this->printed = false; |
| 436 |
|
| 437 |
wp_send_json_success(['html' => $this->get_markup(false)]); |
| 438 |
} |
| 439 |
|
| 440 |
/** |
| 441 |
* Cart page placement. |
| 442 |
* |
| 443 |
* @return void |
| 444 |
*/ |
| 445 |
public function render_for_cart(): void |
| 446 |
{ |
| 447 |
// Cart Table / coupon AJAX re-renders cart/cart.php. That template |
| 448 |
// fires woocommerce_before_cart, which would nest a second bar inside |
| 449 |
// the table fragment and leave the page-level bar stale. |
| 450 |
if (wp_doing_ajax() || $this->printed || empty($this->settings['show_on_cart'])) { |
| 451 |
return; |
| 452 |
} |
| 453 |
|
| 454 |
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped |
| 455 |
echo $this->get_markup(); |
| 456 |
} |
| 457 |
|
| 458 |
/** |
| 459 |
* Checkout placement. |
| 460 |
* |
| 461 |
* @return void |
| 462 |
*/ |
| 463 |
public function render_for_checkout(): void |
| 464 |
{ |
| 465 |
if (wp_doing_ajax() || $this->printed || empty($this->settings['show_on_checkout'])) { |
| 466 |
return; |
| 467 |
} |
| 468 |
|
| 469 |
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped |
| 470 |
echo $this->get_markup(); |
| 471 |
} |
| 472 |
|
| 473 |
/** |
| 474 |
* Mini-cart placement. |
| 475 |
* |
| 476 |
* @return void |
| 477 |
*/ |
| 478 |
public function render_for_mini_cart(): void |
| 479 |
{ |
| 480 |
if (empty($this->settings['show_in_mini_cart'])) { |
| 481 |
return; |
| 482 |
} |
| 483 |
|
| 484 |
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped |
| 485 |
echo $this->get_markup(); |
| 486 |
} |
| 487 |
|
| 488 |
/** |
| 489 |
* Refresh the bar through WooCommerce's own fragment mechanism. |
| 490 |
* |
| 491 |
* @param array<string, string> $fragments Cart fragments. |
| 492 |
* |
| 493 |
* @return array<string, string> |
| 494 |
*/ |
| 495 |
public function add_cart_fragment(array $fragments): array |
| 496 |
{ |
| 497 |
if (!$this->is_active() || empty($this->settings['show_in_mini_cart'])) { |
| 498 |
return $fragments; |
| 499 |
} |
| 500 |
|
| 501 |
$markup = $this->get_markup(); |
| 502 |
if ('' !== $markup) { |
| 503 |
$fragments['div.king-addons-fsb'] = $markup; |
| 504 |
} |
| 505 |
|
| 506 |
return $fragments; |
| 507 |
} |
| 508 |
|
| 509 |
/** |
| 510 |
* Front-end assets. |
| 511 |
* |
| 512 |
* @return void |
| 513 |
*/ |
| 514 |
public function maybe_enqueue_assets(): void |
| 515 |
{ |
| 516 |
if (!$this->is_active()) { |
| 517 |
return; |
| 518 |
} |
| 519 |
|
| 520 |
$dir = KING_ADDONS_PATH . 'includes/extensions/Free_Shipping_Bar/assets/'; |
| 521 |
$css_ver = file_exists($dir . 'style.css') ? (string) filemtime($dir . 'style.css') : KING_ADDONS_VERSION; |
| 522 |
$js_ver = file_exists($dir . 'script.js') ? (string) filemtime($dir . 'script.js') : KING_ADDONS_VERSION; |
| 523 |
|
| 524 |
wp_enqueue_style( |
| 525 |
KING_ADDONS_ASSETS_UNIQUE_KEY . '-free-shipping-bar', |
| 526 |
KING_ADDONS_URL . 'includes/extensions/Free_Shipping_Bar/assets/style.css', |
| 527 |
[], |
| 528 |
$css_ver |
| 529 |
); |
| 530 |
|
| 531 |
wp_enqueue_script( |
| 532 |
KING_ADDONS_ASSETS_UNIQUE_KEY . '-free-shipping-bar', |
| 533 |
KING_ADDONS_URL . 'includes/extensions/Free_Shipping_Bar/assets/script.js', |
| 534 |
[], |
| 535 |
$js_ver, |
| 536 |
true |
| 537 |
); |
| 538 |
|
| 539 |
wp_localize_script( |
| 540 |
KING_ADDONS_ASSETS_UNIQUE_KEY . '-free-shipping-bar', |
| 541 |
'kingAddonsFsb', |
| 542 |
[ |
| 543 |
'ajaxUrl' => admin_url('admin-ajax.php'), |
| 544 |
'nonce' => wp_create_nonce('king_addons_fsb'), |
| 545 |
] |
| 546 |
); |
| 547 |
} |
| 548 |
|
| 549 |
/** |
| 550 |
* Admin submenu. |
| 551 |
* |
| 552 |
* @return void |
| 553 |
*/ |
| 554 |
public function register_admin_menu(): void |
| 555 |
{ |
| 556 |
add_submenu_page( |
| 557 |
king_addons_woo_admin_parent_slug(), |
| 558 |
esc_html__('Free Shipping Bar', 'king-addons'), |
| 559 |
esc_html__('Free Shipping Bar', 'king-addons'), |
| 560 |
'manage_options', |
| 561 |
'king-addons-free-shipping-bar', |
| 562 |
[$this, 'render_admin_page'] |
| 563 |
); |
| 564 |
} |
| 565 |
|
| 566 |
/** |
| 567 |
* Save handler. |
| 568 |
* |
| 569 |
* @return void |
| 570 |
*/ |
| 571 |
public function handle_save(): void |
| 572 |
{ |
| 573 |
if (!current_user_can('manage_options')) { |
| 574 |
wp_die(esc_html__('You are not allowed to do this.', 'king-addons')); |
| 575 |
} |
| 576 |
|
| 577 |
check_admin_referer('king_addons_fsb_save'); |
| 578 |
|
| 579 |
$raw = isset($_POST['ka_fsb']) && is_array($_POST['ka_fsb']) |
| 580 |
? wp_unslash($_POST['ka_fsb']) // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized |
| 581 |
: []; |
| 582 |
|
| 583 |
$defaults = $this->get_default_settings(); |
| 584 |
|
| 585 |
$clean = [ |
| 586 |
'enabled' => !empty($raw['enabled']), |
| 587 |
'threshold_source' => in_array(($raw['threshold_source'] ?? 'auto'), ['auto', 'manual'], true) |
| 588 |
? $raw['threshold_source'] : 'auto', |
| 589 |
'manual_amount' => is_numeric($raw['manual_amount'] ?? '') ? (float) $raw['manual_amount'] : '', |
| 590 |
'show_on_cart' => !empty($raw['show_on_cart']) ? 'yes' : '', |
| 591 |
'show_on_checkout' => !empty($raw['show_on_checkout']) ? 'yes' : '', |
| 592 |
'show_in_mini_cart' => !empty($raw['show_in_mini_cart']) ? 'yes' : '', |
| 593 |
'text_progress' => sanitize_text_field((string) ($raw['text_progress'] ?? $defaults['text_progress'])), |
| 594 |
'text_success' => sanitize_text_field((string) ($raw['text_success'] ?? $defaults['text_success'])), |
| 595 |
'bar_height' => max(2, min(40, (int) ($raw['bar_height'] ?? $defaults['bar_height']))), |
| 596 |
'bar_color' => sanitize_hex_color((string) ($raw['bar_color'] ?? '')) ?: $defaults['bar_color'], |
| 597 |
'bar_track_color' => sanitize_hex_color((string) ($raw['bar_track_color'] ?? '')) ?: $defaults['bar_track_color'], |
| 598 |
'text_color' => sanitize_hex_color((string) ($raw['text_color'] ?? '')) ?: $defaults['text_color'], |
| 599 |
'hide_when_reached' => !empty($raw['hide_when_reached']) ? 'yes' : '', |
| 600 |
]; |
| 601 |
|
| 602 |
update_option(self::OPTION_KEY, $clean); |
| 603 |
|
| 604 |
wp_safe_redirect(add_query_arg( |
| 605 |
['page' => 'king-addons-free-shipping-bar', 'ka-saved' => '1'], |
| 606 |
admin_url('admin.php') |
| 607 |
)); |
| 608 |
exit; |
| 609 |
} |
| 610 |
|
| 611 |
/** |
| 612 |
* Settings screen. |
| 613 |
* |
| 614 |
* @return void |
| 615 |
*/ |
| 616 |
public function render_admin_page(): void |
| 617 |
{ |
| 618 |
if (!current_user_can('manage_options')) { |
| 619 |
return; |
| 620 |
} |
| 621 |
|
| 622 |
$s = $this->get_settings(); |
| 623 |
$detected = $this->get_threshold(); |
| 624 |
|
| 625 |
require __DIR__ . '/templates/admin-page.php'; |
| 626 |
} |
| 627 |
} |
| 628 |
|