PluginProbe
King Addons for Elementor – 100+ Elementor Widgets, 4 000+ Elementor Templates, WooCommerce Builder, Mega Menu, Popup Builder / 51.1.44
King Addons for Elementor – 100+ Elementor Widgets, 4 000+ Elementor Templates, WooCommerce Builder, Mega Menu, Popup Builder v51.1.44
51.1.86 51.1.84 51.1.85 51.1.83 51.1.82 51.1.81 51.1.79 51.1.78 51.1.77 51.1.76 51.1.74 51.1.75 51.1.65 51.1.64 51.1.63 trunk 51.1.14 51.1.2 51.1.35 51.1.36 51.1.37 51.1.38 51.1.39 51.1.44 51.1.45 All 40 releases
king-addons / includes / extensions / Woo_Builder / Woo_Builder.php

Woo_Builder.php in King Addons for Elementor – 100+ Elementor Widgets, 4 000+ Elementor Templates, WooCommerce Builder, Mega Menu, Popup Builder 51.1.44, at includes/extensions/Woo_Builder/Woo_Builder.php

1,004 lines 35.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * WooCommerce Builder feature.
4 *
5 * @package King_Addons
6 */
7
8 namespace King_Addons;
9
10 use Elementor\Plugin as Elementor_Plugin;
11 use King_Addons\Woo_Builder\Context as Woo_Context;
12 use King_Addons\Woo_Builder\Document as Woo_Document;
13 use King_Addons\Woo_Builder\My_Account_Manager;
14 use WP_Post;
15 use WP_Query;
16
17 if (!defined('ABSPATH')) {
18 exit;
19 }
20
21 /**
22 * Registers and renders Elementor-based WooCommerce templates.
23 */
24 class Woo_Builder
25 {
26 /**
27 * Current template IDs keyed by context.
28 *
29 * @var array<string,int>
30 */
31 private array $current_templates = [];
32
33 /**
34 * Active context for body classes.
35 *
36 * @var string|null
37 */
38 private ?string $active_context = null;
39
40 /**
41 * Flag to ensure default blocks hook only once.
42 *
43 * @var bool
44 */
45 private bool $default_blocks_registered = false;
46
47 /**
48 * Constructor.
49 */
50 public function __construct()
51 {
52 if (!class_exists('WooCommerce') || !did_action('elementor/loaded')) {
53 return;
54 }
55
56 require_once KING_ADDONS_PATH . 'includes/helpers/Woo_Builder/Context.php';
57
58 add_action('init', [$this, 'register_template_types']);
59 add_action('wp_enqueue_scripts', [$this, 'register_assets']);
60 add_filter('template_include', [$this, 'override_wc_template'], 99);
61 add_filter('king_addons/woo_builder/current_template_id', [$this, 'filter_current_template_id'], 10, 2);
62 add_filter('body_class', [$this, 'filter_body_class']);
63
64 // My Account endpoint manager (Pro routing).
65 add_action(
66 'plugins_loaded',
67 static function () {
68 if (!class_exists(My_Account_Manager::class)) {
69 require_once KING_ADDONS_PATH . 'includes/helpers/Woo_Builder/My_Account_Manager.php';
70 }
71 new My_Account_Manager();
72 }
73 );
74 add_filter('default_post_metadata', [$this, 'prefill_template_meta'], 10, 5);
75
76 add_action('add_meta_boxes', [$this, 'register_meta_boxes']);
77 add_action('save_post', [$this, 'save_template_meta'], 10, 2);
78 }
79
80 /**
81 * Register assets for builder.
82 *
83 * @return void
84 */
85 public function register_assets(): void
86 {
87 $deps = is_admin() ? ['jquery', 'select2', 'wp-api'] : [];
88
89 wp_register_style(
90 KING_ADDONS_ASSETS_UNIQUE_KEY . '-woo-builder-style',
91 KING_ADDONS_URL . 'includes/extensions/Woo_Builder/style.css',
92 [],
93 KING_ADDONS_VERSION
94 );
95
96 wp_register_script(
97 KING_ADDONS_ASSETS_UNIQUE_KEY . '-woo-builder-script',
98 KING_ADDONS_URL . 'includes/extensions/Woo_Builder/script.js',
99 $deps,
100 KING_ADDONS_VERSION,
101 true
102 );
103 }
104
105 /**
106 * Register Elementor document type for Woo Builder.
107 *
108 * @return void
109 */
110 public function register_template_types(): void
111 {
112 if (!class_exists('Elementor\\Plugin')) {
113 return;
114 }
115
116 if (!class_exists('King_Addons\\Woo_Builder\\Document')) {
117 require_once KING_ADDONS_PATH . 'includes/helpers/Woo_Builder/Document.php';
118 }
119
120 if (class_exists('King_Addons\\Woo_Builder\\Document')) {
121 Elementor_Plugin::$instance->documents->register_document_type(
122 Woo_Document::get_type(),
123 Woo_Document::class
124 );
125 }
126 }
127
128 /**
129 * Register meta box for Elementor templates.
130 *
131 * @return void
132 */
133 public function register_meta_boxes(): void
134 {
135 add_meta_box(
136 'king-addons-woo-builder',
137 esc_html__('Woo Builder Conditions', 'king-addons'),
138 [$this, 'render_meta_box'],
139 'elementor_library',
140 'side',
141 'default'
142 );
143 }
144
145 /**
146 * Render meta box UI.
147 *
148 * @param WP_Post $post Post object.
149 *
150 * @return void
151 */
152 public function render_meta_box(WP_Post $post): void
153 {
154 wp_nonce_field('king_addons_woo_builder_meta', 'king_addons_woo_builder_meta_nonce');
155
156 $template_type = get_post_meta($post->ID, 'ka_woo_template_type', true);
157 $conditions = get_post_meta($post->ID, 'ka_woo_conditions', true);
158 $enabled = !empty($conditions['enabled']);
159 $priority = isset($conditions['priority']) ? (int) $conditions['priority'] : 10;
160 $rules = [];
161 if (!empty($conditions['rules']) && is_array($conditions['rules'])) {
162 $rules = $conditions['rules'];
163 }
164 foreach ($rules as $idx => $rule) {
165 $rules[$idx]['type'] = $this->normalize_rule_type($rule['type'] ?? '');
166 }
167 if (empty($rules)) {
168 $rules[] = ['type' => 'always', 'values' => []];
169 }
170
171 $types = [
172 '' => esc_html__('Select type', 'king-addons'),
173 'single_product' => esc_html__('Single Product', 'king-addons'),
174 'product_archive' => esc_html__('Product Archive', 'king-addons'),
175 'cart' => esc_html__('Cart', 'king-addons'),
176 'checkout' => esc_html__('Checkout', 'king-addons'),
177 'my_account' => esc_html__('My Account', 'king-addons'),
178 ];
179
180 $rule_types = [
181 'always' => esc_html__('Always', 'king-addons'),
182 'all_products' => esc_html__('All products', 'king-addons'),
183 'product_in' => esc_html__('Specific products', 'king-addons'),
184 'product_cat_in' => esc_html__('Product categories', 'king-addons'),
185 'product_tag_in' => esc_html__('Product tags', 'king-addons'),
186 'product_type_in' => esc_html__('Product types', 'king-addons'),
187 'is_shop' => esc_html__('Shop page', 'king-addons'),
188 'product_cat_archive_in' => esc_html__('Product category archives', 'king-addons'),
189 'cart' => esc_html__('Cart page', 'king-addons'),
190 'checkout' => esc_html__('Checkout page', 'king-addons'),
191 'my_account' => esc_html__('My Account page', 'king-addons'),
192 // Legacy aliases kept for backward compatibility.
193 'products' => esc_html__('Specific products (IDs)', 'king-addons') . ' (legacy)',
194 'product_categories' => esc_html__('Product categories (IDs)', 'king-addons') . ' (legacy)',
195 'product_tags' => esc_html__('Product tags (IDs)', 'king-addons') . ' (legacy)',
196 'product_types' => esc_html__('Product types (slugs)', 'king-addons') . ' (legacy)',
197 'shop' => esc_html__('Shop page', 'king-addons') . ' (legacy)',
198 'product_cat_archives' => esc_html__('Product category archives (IDs)', 'king-addons') . ' (legacy)',
199 ];
200
201 // Admin assets for select2 UI and REST.
202 wp_enqueue_style('select2');
203 wp_enqueue_script('select2');
204 wp_enqueue_script('wp-api');
205 wp_enqueue_script(KING_ADDONS_ASSETS_UNIQUE_KEY . '-woo-builder-script');
206 wp_enqueue_style(KING_ADDONS_ASSETS_UNIQUE_KEY . '-woo-builder-style');
207 wp_localize_script(
208 KING_ADDONS_ASSETS_UNIQUE_KEY . '-woo-builder-script',
209 'KingAddonsWooBuilder',
210 [
211 'labels' => [
212 'valuePlaceholder' => esc_html__('Search or select…', 'king-addons'),
213 ],
214 ]
215 );
216 ?>
217 <p>
218 <label>
219 <input type="checkbox" name="ka_woo_enabled" value="1" <?php checked($enabled); ?> />
220 <?php esc_html_e('Enable template', 'king-addons'); ?>
221 </label>
222 </p>
223 <p>
224 <label for="ka_woo_template_type"><strong><?php esc_html_e('Template Type', 'king-addons'); ?></strong></label><br />
225 <select name="ka_woo_template_type" id="ka_woo_template_type" style="width:100%;">
226 <?php foreach ($types as $value => $label) : ?>
227 <option value="<?php echo esc_attr($value); ?>" <?php selected($template_type, $value); ?>><?php echo esc_html($label); ?></option>
228 <?php endforeach; ?>
229 </select>
230 </p>
231 <?php if (!$this->can_use_pro()) : ?>
232 <p class="king-addons-woo-builder-pro-lock">
233 <?php esc_html_e('Cart, Checkout, and My Account templates require the Pro version.', 'king-addons'); ?>
234 </p>
235 <?php endif; ?>
236 <p>
237 <label for="ka_woo_priority"><strong><?php esc_html_e('Priority', 'king-addons'); ?></strong></label><br />
238 <input type="number" min="0" name="ka_woo_priority" id="ka_woo_priority" value="<?php echo esc_attr($priority); ?>" style="width:100%;" />
239 </p>
240 <div id="ka-woo-rules">
241 <strong><?php esc_html_e('Rules (AND)', 'king-addons'); ?></strong>
242 <?php foreach ($rules as $index => $rule) : ?>
243 <div class="ka-woo-rule" style="border:1px solid #e0e0e0;padding:8px;margin-top:8px;border-radius:4px;">
244 <?php
245 $rule_type_value = $rule['type'] ?? '';
246 if ($rule_type_value && !isset($rule_types[$rule_type_value])) {
247 $rule_types[$rule_type_value] = sprintf(
248 /* translators: %s: rule type slug */
249 esc_html__('Unknown rule (%s)', 'king-addons'),
250 esc_html($rule_type_value)
251 );
252 }
253 ?>
254 <select class="ka-woo-rule-type" name="ka_woo_rule_type[<?php echo esc_attr($index); ?>]" style="width:100%;margin-bottom:6px;">
255 <?php foreach ($rule_types as $value => $label) : ?>
256 <option value="<?php echo esc_attr($value); ?>" <?php selected($rule['type'] ?? '', $value); ?>><?php echo esc_html($label); ?></option>
257 <?php endforeach; ?>
258 </select>
259 <?php
260 $rule_values = isset($rule['values']) && is_array($rule['values']) ? $rule['values'] : [];
261 $value_options = $this->get_rule_value_options($rule['type'] ?? '', $rule_values);
262 ?>
263 <select
264 class="ka-woo-rule-values"
265 name="ka_woo_rule_values[<?php echo esc_attr($index); ?>][]"
266 multiple
267 data-rule-type="<?php echo esc_attr($rule['type'] ?? ''); ?>"
268 data-selected-ids="<?php echo esc_attr(implode(',', array_map('strval', $rule_values))); ?>"
269 style="width:100%;"
270 >
271 <?php foreach ($value_options as $option_value => $option_label) : ?>
272 <option value="<?php echo esc_attr($option_value); ?>" <?php selected(true, in_array((string) $option_value, array_map('strval', $rule_values), true)); ?>>
273 <?php echo esc_html($option_label); ?>
274 </option>
275 <?php endforeach; ?>
276 </select>
277 <button type="button" class="button ka-woo-remove-rule" style="margin-top:6px;"><?php esc_html_e('Remove', 'king-addons'); ?></button>
278 </div>
279 <?php endforeach; ?>
280 </div>
281 <p><button type="button" class="button" id="ka-woo-add-rule"><?php esc_html_e('Add rule', 'king-addons'); ?></button></p>
282 <?php
283 }
284
285 /**
286 * Save meta box data.
287 *
288 * @param int $post_id Post ID.
289 * @param WP_Post $post Post.
290 *
291 * @return void
292 */
293 public function save_template_meta(int $post_id, WP_Post $post): void
294 {
295 if ('elementor_library' !== $post->post_type) {
296 return;
297 }
298
299 if (!isset($_POST['king_addons_woo_builder_meta_nonce']) || !wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['king_addons_woo_builder_meta_nonce'])), 'king_addons_woo_builder_meta')) {
300 return;
301 }
302
303 if (!current_user_can('edit_post', $post_id)) {
304 return;
305 }
306
307 $template_type = sanitize_text_field($_POST['ka_woo_template_type'] ?? '');
308 update_post_meta($post_id, 'ka_woo_template_type', $template_type);
309
310 $enabled = !empty($_POST['ka_woo_enabled']);
311 $priority = isset($_POST['ka_woo_priority']) ? (int) $_POST['ka_woo_priority'] : 10;
312
313 $types = $_POST['ka_woo_rule_type'] ?? [];
314 $values = $_POST['ka_woo_rule_values'] ?? [];
315
316 $rules = [];
317 if (is_array($types) && is_array($values)) {
318 foreach ($types as $idx => $type) {
319 $type = sanitize_text_field($type);
320 if ('' === $type) {
321 continue;
322 }
323 $val_raw = $values[$idx] ?? '';
324 if (is_array($val_raw)) {
325 $vals = array_filter(array_map('trim', array_map('strval', $val_raw)));
326 } else {
327 $vals = array_filter(array_map('trim', explode(',', (string) $val_raw)));
328 }
329 $parsed_vals = [];
330 foreach ($vals as $v) {
331 if (is_numeric($v)) {
332 $parsed_vals[] = (int) $v;
333 } else {
334 $parsed_vals[] = sanitize_text_field($v);
335 }
336 }
337 $rules[] = [
338 'type' => $this->normalize_rule_type($type),
339 'values' => $parsed_vals,
340 ];
341 }
342 }
343
344 $conditions = [
345 'enabled' => $enabled,
346 'priority' => $priority,
347 'rules' => $rules,
348 ];
349
350 update_post_meta($post_id, 'ka_woo_conditions', $conditions);
351 }
352
353 /**
354 * Template include override.
355 *
356 * @param string $template Default template.
357 *
358 * @return string
359 */
360 public function override_wc_template(string $template): string
361 {
362 $context = Woo_Context::detect_context();
363 if (null === $context) {
364 return $template;
365 }
366
367 if (!$this->can_use_pro() && in_array($context, ['cart', 'checkout', 'my_account'], true)) {
368 return $template;
369 }
370
371 $template_id = $this->get_active_template_for_context($context);
372 $this->current_templates[$context] = $template_id ?: 0;
373 $this->active_context = $template_id ? $context : null;
374
375 if (!$template_id) {
376 return $template;
377 }
378
379 $this->maybe_register_default_blocks();
380
381 $wrapper = KING_ADDONS_PATH . 'includes/extensions/Woo_Builder/templates/' . $context . '.php';
382 if (file_exists($wrapper)) {
383 return $wrapper;
384 }
385
386 return $template;
387 }
388
389 /**
390 * Get active template ID for context.
391 *
392 * @param string $context Context.
393 *
394 * @return int|null
395 */
396 public function get_active_template_for_context(string $context): ?int
397 {
398 $query = new WP_Query([
399 'post_type' => 'elementor_library',
400 'posts_per_page' => -1,
401 'post_status' => 'publish',
402 'meta_query' => [
403 [
404 'key' => 'ka_woo_template_type',
405 'value' => $context,
406 ],
407 ],
408 ]);
409
410 if (!$query->have_posts()) {
411 return null;
412 }
413
414 $candidates = [];
415
416 foreach ($query->posts as $post) {
417 $conditions = get_post_meta($post->ID, 'ka_woo_conditions', true);
418 if (!is_array($conditions) || empty($conditions['enabled'])) {
419 continue;
420 }
421
422 if (!$this->can_use_pro() && !in_array($context, ['single_product', 'product_archive'], true)) {
423 continue;
424 }
425
426 if (!Woo_Context::match_conditions($context, $conditions)) {
427 continue;
428 }
429
430 $priority = isset($conditions['priority']) ? (int) $conditions['priority'] : 10;
431 $candidates[] = [
432 'id' => (int) $post->ID,
433 'priority' => $priority,
434 ];
435 }
436
437 if (empty($candidates)) {
438 return null;
439 }
440
441 usort(
442 $candidates,
443 static function ($a, $b) {
444 return $a['priority'] <=> $b['priority'];
445 }
446 );
447
448 // Free: limit one template per context by picking top; Pro may have multiple but we still pick top priority.
449 return (int) $candidates[0]['id'];
450 }
451
452 /**
453 * Filter to expose current template id.
454 *
455 * @param int $template_id Template ID.
456 * @param string|null $context Context.
457 *
458 * @return int
459 */
460 public function filter_current_template_id(int $template_id, ?string $context = null): int
461 {
462 if ($context && !empty($this->current_templates[$context])) {
463 return (int) $this->current_templates[$context];
464 }
465 return $template_id;
466 }
467
468 /**
469 * Add body classes for active Woo Builder context.
470 *
471 * @param array<int,string> $classes Existing classes.
472 *
473 * @return array<int,string>
474 */
475 public function filter_body_class(array $classes): array
476 {
477 if (null !== $this->active_context) {
478 $classes[] = 'king-addons-woo-builder';
479 $classes[] = 'king-addons-woo-builder--' . $this->active_context;
480 if ($this->can_use_pro()) {
481 $classes[] = 'king-addons-woo-builder--pro';
482 }
483 }
484
485 return $classes;
486 }
487
488 /**
489 * Register default Pro blocks (upsells, quick links) once.
490 *
491 * @return void
492 */
493 private function maybe_register_default_blocks(): void
494 {
495 if ($this->default_blocks_registered || !$this->can_use_pro()) {
496 return;
497 }
498
499 add_action('king_addons/woo_builder/after_render', [$this, 'render_default_block'], 10, 2);
500 $this->default_blocks_registered = true;
501 }
502
503 /**
504 * Render default Pro blocks for supported contexts.
505 *
506 * @param string $context Context.
507 * @param int $template_id Template id.
508 *
509 * @return void
510 */
511 public function render_default_block(string $context, int $template_id): void
512 {
513 if (!$this->can_use_pro()) {
514 return;
515 }
516
517 $disable_meta = get_post_meta($template_id, 'ka_woo_disable_autoblocks', true);
518 if (!empty($disable_meta)) {
519 return;
520 }
521
522 if ('checkout' === $context) {
523 $this->render_checkout_upsells();
524 } elseif ('cart' === $context) {
525 $this->render_cart_cross_sells();
526 } elseif ('my_account' === $context) {
527 $this->render_account_quick_links();
528 }
529 }
530
531 /**
532 * Render checkout upsells block.
533 *
534 * @return void
535 */
536 private function render_checkout_upsells(): void
537 {
538 if (!function_exists('WC') || !WC()->cart || apply_filters('king_addons/woo_builder/disable_checkout_upsells', false)) {
539 return;
540 }
541
542 $cart_items = WC()->cart->get_cart();
543 $upsell_ids = [];
544 foreach ($cart_items as $item) {
545 $product = $item['data'] ?? null;
546 if ($product && is_object($product) && method_exists($product, 'get_upsell_ids')) {
547 $upsell_ids = array_merge($upsell_ids, $product->get_upsell_ids());
548 }
549 }
550 $upsell_ids = array_values(array_unique(array_filter(array_map('absint', $upsell_ids))));
551
552 /**
553 * Allow custom upsell IDs (e.g., personalization/A/B).
554 *
555 * @param array<int> $upsell_ids Upsell product IDs.
556 * @param array $cart_items Cart contents.
557 */
558 $upsell_ids = apply_filters('king_addons/woo_builder/checkout_upsell_ids', $upsell_ids, $cart_items);
559
560 if (empty($upsell_ids)) {
561 // Fallback: same categories as cart items.
562 $cat_ids = [];
563 $tag_ids = [];
564 foreach ($cart_items as $item) {
565 $product_id = isset($item['product_id']) ? (int) $item['product_id'] : 0;
566 if ($product_id) {
567 $terms = wp_get_post_terms($product_id, 'product_cat', ['fields' => 'ids']);
568 $cat_ids = array_merge($cat_ids, $terms);
569 $tags = wp_get_post_terms($product_id, 'product_tag', ['fields' => 'ids']);
570 $tag_ids = array_merge($tag_ids, $tags);
571 }
572 }
573 $cat_ids = array_values(array_unique(array_filter(array_map('absint', $cat_ids))));
574 $tag_ids = array_values(array_unique(array_filter(array_map('absint', $tag_ids))));
575 if (!empty($cat_ids)) {
576 $upsell_ids = get_posts(
577 [
578 'post_type' => 'product',
579 'fields' => 'ids',
580 'numberposts' => (int) apply_filters('king_addons/woo_builder/checkout_upsells_fallback_limit', 6),
581 'tax_query' => [
582 [
583 'taxonomy' => 'product_cat',
584 'field' => 'term_id',
585 'terms' => $cat_ids,
586 ],
587 ],
588 ]
589 );
590 } elseif (!empty($tag_ids)) {
591 $upsell_ids = get_posts(
592 [
593 'post_type' => 'product',
594 'fields' => 'ids',
595 'numberposts' => (int) apply_filters('king_addons/woo_builder/checkout_upsells_tag_limit', 6),
596 'tax_query' => [
597 [
598 'taxonomy' => 'product_tag',
599 'field' => 'term_id',
600 'terms' => $tag_ids,
601 ],
602 ],
603 ]
604 );
605 }
606 }
607
608 if (empty($upsell_ids)) {
609 return;
610 }
611
612 $limit = (int) apply_filters('king_addons/woo_builder/checkout_upsells_limit', 4);
613 $query = new WP_Query(
614 apply_filters(
615 'king_addons/woo_builder/checkout_upsells_query_args',
616 [
617 'post_type' => 'product',
618 'post__in' => $upsell_ids,
619 'posts_per_page' => $limit,
620 'orderby' => 'post__in',
621 ]
622 )
623 );
624
625 if (!$query->have_posts()) {
626 wp_reset_postdata();
627 return;
628 }
629
630 $title = apply_filters('king_addons/woo_builder/checkout_upsells_title', esc_html__('You may also like', 'king-addons'));
631
632 echo '<section class="ka-woo-checkout-upsells" aria-label="' . esc_attr($title) . '">';
633 if ($title) {
634 echo '<h3 class="ka-woo-checkout-upsells__title">' . esc_html($title) . '</h3>';
635 }
636 echo '<div class="ka-woo-checkout-upsells__grid">';
637 while ($query->have_posts()) {
638 $query->the_post();
639 $product = wc_get_product(get_the_ID());
640 if (!$product) {
641 continue;
642 }
643 $this->render_simple_product_card($product);
644 }
645 echo '</div>';
646 echo '</section>';
647 wp_reset_postdata();
648 }
649
650 /**
651 * Render cart cross-sells fallback.
652 *
653 * @return void
654 */
655 private function render_cart_cross_sells(): void
656 {
657 if (!function_exists('woocommerce_cross_sell_display') || apply_filters('king_addons/woo_builder/disable_cart_cross_sells', false)) {
658 return;
659 }
660 $cart_items = (function_exists('WC') && WC()->cart) ? WC()->cart->get_cart() : [];
661 $suggested_ids = [];
662 foreach ($cart_items as $item) {
663 $product_id = isset($item['product_id']) ? (int) $item['product_id'] : 0;
664 if ($product_id) {
665 $tags = wp_get_post_terms($product_id, 'product_tag', ['fields' => 'ids']);
666 $suggested_ids = array_merge($suggested_ids, $tags);
667 }
668 }
669 $suggested_ids = array_values(array_unique(array_filter(array_map('absint', $suggested_ids))));
670 $suggested_args = [];
671 if (!empty($suggested_ids)) {
672 $suggested_args = [
673 'post_type' => 'product',
674 'posts_per_page' => (int) apply_filters('king_addons/woo_builder/cart_cross_sells_suggested_limit', 4),
675 'tax_query' => [
676 [
677 'taxonomy' => 'product_tag',
678 'field' => 'term_id',
679 'terms' => $suggested_ids,
680 ],
681 ],
682 ];
683 }
684
685 /**
686 * Allow overriding cross-sell query (e.g., custom tags/meta).
687 *
688 * @param array<string,mixed> $args Query args, empty for Woo default.
689 * @param array $cart_items Cart items.
690 */
691 $suggested_args = apply_filters('king_addons/woo_builder/cart_cross_sells_query_args', $suggested_args, $cart_items);
692
693 $title = apply_filters('king_addons/woo_builder/cart_cross_sells_title', esc_html__('You may also like', 'king-addons'));
694 echo '<section class="ka-woo-cart-cross-sells" aria-label="' . esc_attr($title) . '">';
695 if ($title) {
696 echo '<h3 class="ka-woo-cart-cross-sells__title">' . esc_html($title) . '</h3>';
697 }
698 if (!empty($suggested_args)) {
699 $loop = new WP_Query($suggested_args);
700 if ($loop->have_posts()) {
701 echo '<div class="ka-woo-checkout-upsells__grid">';
702 while ($loop->have_posts()) {
703 $loop->the_post();
704 $product = wc_get_product(get_the_ID());
705 if ($product) {
706 $this->render_simple_product_card($product);
707 }
708 }
709 echo '</div>';
710 }
711 wp_reset_postdata();
712 } else {
713 woocommerce_cross_sell_display(
714 (int) apply_filters('king_addons/woo_builder/cart_cross_sells_limit', 4),
715 (int) apply_filters('king_addons/woo_builder/cart_cross_sells_columns', 4)
716 );
717 }
718 echo '</section>';
719 }
720
721 /**
722 * Render My Account quick links.
723 *
724 * @return void
725 */
726 private function render_account_quick_links(): void
727 {
728 if (!function_exists('wc_get_account_endpoint_url') || apply_filters('king_addons/woo_builder/disable_account_quick_links', false)) {
729 return;
730 }
731
732 $links = apply_filters(
733 'king_addons/woo_builder/account_quick_links',
734 [
735 'orders' => esc_html__('Orders', 'king-addons'),
736 'downloads' => esc_html__('Downloads', 'king-addons'),
737 'edit-address' => esc_html__('Addresses', 'king-addons'),
738 'payment-methods' => esc_html__('Payment Methods', 'king-addons'),
739 'edit-account' => esc_html__('Account Details', 'king-addons'),
740 ]
741 );
742
743 echo '<nav class="ka-woo-account-quick-links" aria-label="' . esc_attr__('Account quick links', 'king-addons') . '">';
744 echo '<ul class="ka-woo-account-quick-links__list">';
745 foreach ($links as $endpoint => $label) {
746 $url = wc_get_account_endpoint_url($endpoint);
747 if (!$url) {
748 continue;
749 }
750 echo '<li class="ka-woo-account-quick-links__item">';
751 echo '<a class="ka-woo-account-quick-links__link" href="' . esc_url($url) . '">' . esc_html($label) . '</a>';
752 echo '</li>';
753 }
754 echo '</ul>';
755 echo '</nav>';
756
757 /**
758 * Hook to output additional custom blocks in My Account (Pro).
759 *
760 * @param array<string,string> $links Current quick links.
761 */
762 do_action('king_addons/woo_builder/account_after_quick_links', $links);
763 }
764
765 /**
766 * Render a simple product card for upsells.
767 *
768 * @param \WC_Product $product Product instance.
769 *
770 * @return void
771 */
772 private function render_simple_product_card(\WC_Product $product): void
773 {
774 $link = get_permalink($product->get_id());
775 $image = $product->get_image('woocommerce_thumbnail');
776 $title = $product->get_name();
777 $price = $product->get_price_html();
778
779 echo '<article class="ka-woo-checkout-upsells__item">';
780 echo '<a class="ka-woo-checkout-upsells__thumb" href="' . esc_url($link) . '">' . wp_kses_post($image) . '</a>';
781 echo '<h4 class="ka-woo-checkout-upsells__title"><a href="' . esc_url($link) . '">' . esc_html($title) . '</a></h4>';
782 if (!empty($price)) {
783 echo '<div class="ka-woo-checkout-upsells__price">' . wp_kses_post($price) . '</div>';
784 }
785 echo $this->render_add_to_cart_button($product); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
786 echo '</article>';
787 }
788
789 /**
790 * Render a controlled Add to Cart button without relying on Woo shortcode.
791 *
792 * @param \WC_Product $product Product instance.
793 *
794 * @return string
795 */
796 private function render_add_to_cart_button(\WC_Product $product): string
797 {
798 if (!$product->is_purchasable() || !$product->is_in_stock()) {
799 return '<span class="ka-woo-checkout-upsells__add-to-cart button is-disabled">' . esc_html__('Unavailable', 'king-addons') . '</span>';
800 }
801
802 if (function_exists('wp_enqueue_script') && function_exists('wp_script_is') && wp_script_is('wc-add-to-cart', 'registered')) {
803 wp_enqueue_script('wc-add-to-cart');
804 }
805
806 $classes = [
807 'ka-woo-checkout-upsells__add-to-cart',
808 'button',
809 'product_type_' . $product->get_type(),
810 ];
811
812 if ($product->supports('ajax_add_to_cart')) {
813 $classes[] = 'ajax_add_to_cart';
814 $classes[] = 'add_to_cart_button';
815 }
816
817 $attributes = [
818 'href' => $product->add_to_cart_url(),
819 'data-quantity' => 1,
820 'data-product_id' => $product->get_id(),
821 'data-product_sku' => $product->get_sku(),
822 'rel' => 'nofollow',
823 'class' => implode(' ', array_filter($classes)),
824 'aria-label' => wp_strip_all_tags($product->add_to_cart_description()),
825 ];
826
827 return '<a ' . wc_implode_html_attributes($attributes) . '><span class="ka-woo-checkout-upsells__add-to-cart-label">' . esc_html($product->add_to_cart_text()) . '</span></a>';
828 }
829
830 /**
831 * Prefill meta values when creating a new Elementor template via custom links.
832 *
833 * @param mixed $value Default meta value.
834 * @param int $object_id Object ID.
835 * @param string $meta_key Meta key.
836 * @param bool $single Whether meta is single.
837 * @param string|null $meta_type Meta type.
838 *
839 * @return mixed
840 */
841 public function prefill_template_meta($value, int $object_id, string $meta_key, bool $single, ?string $meta_type)
842 {
843 if ('post' !== $meta_type) {
844 return $value;
845 }
846
847 $requested_type = isset($_GET['ka_woo_template_type'])
848 ? sanitize_text_field(wp_unslash($_GET['ka_woo_template_type']))
849 : '';
850
851 if (empty($requested_type)) {
852 return $value;
853 }
854
855 $post_type = isset($_GET['post_type']) ? sanitize_text_field(wp_unslash($_GET['post_type'])) : '';
856
857 if ('elementor_library' !== $post_type) {
858 return $value;
859 }
860
861 if ('ka_woo_template_type' === $meta_key) {
862 return $requested_type;
863 }
864
865 if ('ka_woo_conditions' === $meta_key) {
866 return [
867 'enabled' => true,
868 'priority' => 10,
869 'rules' => [
870 [
871 'type' => 'always',
872 'values' => [],
873 ],
874 ],
875 ];
876 }
877
878 return $value;
879 }
880
881 /**
882 * Check Pro availability.
883 *
884 * @return bool
885 */
886 private function can_use_pro(): bool
887 {
888 if (!function_exists('king_addons_freemius')) {
889 return false;
890 }
891
892 $fs = king_addons_freemius();
893 if (!is_object($fs) || !method_exists($fs, 'can_use_premium_code')) {
894 return false;
895 }
896
897 return (bool) $fs->can_use_premium_code();
898 }
899
900 /**
901 * Map rule values to human-readable labels for select pre-fill.
902 *
903 * @param string $type Rule type.
904 * @param array $values Raw values.
905 *
906 * @return array<string,string>
907 */
908 private function get_rule_value_options(string $type, array $values): array
909 {
910 if (empty($values)) {
911 return [];
912 }
913
914 $options = [];
915 $string_values = array_map('strval', $values);
916
917 switch ($type) {
918 case 'product_in':
919 case 'products':
920 $products = get_posts([
921 'post_type' => 'product',
922 'post__in' => array_map('intval', $values),
923 'numberposts' => -1,
924 ]);
925 foreach ($products as $product) {
926 $options[(string) $product->ID] = $product->post_title;
927 }
928 break;
929 case 'product_cat_in':
930 case 'product_cat_archive_in':
931 case 'product_categories':
932 case 'product_cat_archives':
933 $terms = get_terms([
934 'taxonomy' => 'product_cat',
935 'include' => array_map('intval', $values),
936 'hide_empty' => false,
937 ]);
938 foreach ($terms as $term) {
939 $options[(string) $term->term_id] = $term->name;
940 }
941 break;
942 case 'product_tag_in':
943 case 'product_tags':
944 $terms = get_terms([
945 'taxonomy' => 'product_tag',
946 'include' => array_map('intval', $values),
947 'hide_empty' => false,
948 ]);
949 foreach ($terms as $term) {
950 $options[(string) $term->term_id] = $term->name;
951 }
952 break;
953 case 'product_type_in':
954 case 'product_types':
955 $types = [
956 'simple' => esc_html__('Simple', 'king-addons'),
957 'variable' => esc_html__('Variable', 'king-addons'),
958 'grouped' => esc_html__('Grouped', 'king-addons'),
959 'external' => esc_html__('External/Affiliate', 'king-addons'),
960 ];
961 foreach ($string_values as $v) {
962 if (isset($types[$v])) {
963 $options[$v] = $types[$v];
964 }
965 }
966 break;
967 default:
968 foreach ($string_values as $v) {
969 $options[$v] = $v;
970 }
971 }
972
973 return $options;
974 }
975
976 /**
977 * Normalize legacy rule type slugs to current ones.
978 *
979 * @param string $type Rule type.
980 *
981 * @return string
982 */
983 private function normalize_rule_type(string $type): string
984 {
985 $map = [
986 'products' => 'product_in',
987 'product_categories' => 'product_cat_in',
988 'product_tags' => 'product_tag_in',
989 'product_types' => 'product_type_in',
990 'shop' => 'is_shop',
991 'product_cat_archives' => 'product_cat_archive_in',
992 ];
993
994 return $map[$type] ?? $type;
995 }
996 }
997
998
999
1000
1001
1002
1003
1004