PluginProbe
King Addons for Elementor – 80+ Elementor Widgets, 4 000+ Elementor Templates, WooCommerce, Mega Menu, Popup Builder / 51.1.83
King Addons for Elementor – 80+ Elementor Widgets, 4 000+ Elementor Templates, WooCommerce, Mega Menu, Popup Builder v51.1.83
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 51.1.46 51.1.47 51.1.49 All 37 releases
king-addons / includes / widgets / Woo_Checkout_Form / Woo_Checkout_Form.php

Woo_Checkout_Form.php in King Addons for Elementor – 80+ Elementor Widgets, 4 000+ Elementor Templates, WooCommerce, Mega Menu, Popup Builder 51.1.83, at includes/widgets/Woo_Checkout_Form/Woo_Checkout_Form.php

872 lines 30.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Woo Checkout Form widget.
4 *
5 * @package King_Addons
6 */
7
8 namespace King_Addons;
9
10 use Elementor\Controls_Manager;
11 use Elementor\Group_Control_Border;
12 use Elementor\Group_Control_Box_Shadow;
13 use Elementor\Group_Control_Typography;
14 use King_Addons\Woo_Builder\Context as Woo_Context;
15 if (!defined('ABSPATH')) {
16 exit;
17 }
18
19 /**
20 * Renders the WooCommerce checkout form.
21 */
22 class Woo_Checkout_Form extends Abstract_Checkout_Widget
23 {
24 /**
25 * Last config cache for checkout fields filter.
26 *
27 * @var array<int,array<string,mixed>>
28 */
29 private static array $cached_config = [];
30
31 /**
32 * Last extra fields cache for checkout fields filter.
33 *
34 * @var array<int,array<string,mixed>>
35 */
36 private static array $cached_extra = [];
37
38 /**
39 * Widget slug.
40 *
41 * @return string
42 */
43 public function get_name(): string
44 {
45 return 'woo_checkout_form';
46 }
47
48 /**
49 * Widget title.
50 *
51 * @return string
52 */
53 public function get_title(): string
54 {
55 return esc_html__('Checkout Form', 'king-addons');
56 }
57
58 /**
59 * Widget icon.
60 *
61 * @return string
62 */
63 public function get_icon(): string
64 {
65 return 'king-addons-icon king-addons-woo-checkout-form';
66 }
67
68 /**
69 * Categories.
70 *
71 * @return array<int, string>
72 */
73 public function get_categories(): array
74 {
75 return ['king-addons-woo-builder'];
76 }
77
78 /**
79 * Style dependencies.
80 *
81 * @return array<int, string>
82 */
83 public function get_style_depends(): array
84 {
85 return [KING_ADDONS_ASSETS_UNIQUE_KEY . '-woo-checkout-form-style'];
86 }
87
88 /**
89 * Script dependencies.
90 *
91 * @return array<int, string>
92 */
93 public function get_script_depends(): array
94 {
95 return [KING_ADDONS_ASSETS_UNIQUE_KEY . '-woo-checkout-form-script'];
96 }
97
98 /**
99 * Register controls.
100 *
101 * @return void
102 */
103 protected function register_controls(): void
104 {
105 $this->start_controls_section(
106 'section_content',
107 [
108 'label' => esc_html__('Content', 'king-addons'),
109 'tab' => Controls_Manager::TAB_CONTENT,
110 ]
111 );
112
113 $this->add_control(
114 'show_login',
115 [
116 'label' => sprintf(__('Show Login Notice (Pro) %s', 'king-addons'), '<i class="eicon-pro-icon"></i>'),
117 'type' => Controls_Manager::SWITCHER,
118 'return_value' => 'yes',
119 'default' => 'yes',
120 ]
121 );
122
123 $this->add_control(
124 'show_coupon',
125 [
126 'label' => sprintf(__('Show Coupon Form (Pro) %s', 'king-addons'), '<i class="eicon-pro-icon"></i>'),
127 'type' => Controls_Manager::SWITCHER,
128 'return_value' => 'yes',
129 'default' => 'yes',
130 ]
131 );
132
133 $this->add_control(
134 'show_order_notes',
135 [
136 'label' => sprintf(__('Show Order Notes (Pro) %s', 'king-addons'), '<i class="eicon-pro-icon"></i>'),
137 'type' => Controls_Manager::SWITCHER,
138 'return_value' => 'yes',
139 'default' => 'yes',
140 ]
141 );
142
143 $this->add_control(
144 'enable_field_customization',
145 [
146 'label' => sprintf(__('Customize fields (Pro) %s', 'king-addons'), '<i class="eicon-pro-icon"></i>'),
147 'type' => Controls_Manager::SWITCHER,
148 'return_value' => 'yes',
149 ]
150 );
151
152 $this->add_control(
153 'fields_config',
154 [
155 'label' => esc_html__('Fields settings (label/placeholder/required/order/hide)', 'king-addons'),
156 'type' => Controls_Manager::REPEATER,
157 'condition' => [
158 'enable_field_customization' => 'yes',
159 ],
160 'fields' => [
161 [
162 'name' => 'field_key',
163 'label' => esc_html__('Field key', 'king-addons'),
164 'type' => Controls_Manager::TEXT,
165 'placeholder' => 'billing_phone',
166 ],
167 [
168 'name' => 'section',
169 'label' => esc_html__('Section', 'king-addons'),
170 'type' => Controls_Manager::SELECT,
171 'options' => [
172 'billing' => esc_html__('Billing', 'king-addons'),
173 'shipping' => esc_html__('Shipping', 'king-addons'),
174 'order' => esc_html__('Order notes', 'king-addons'),
175 ],
176 'default' => 'billing',
177 ],
178 [
179 'name' => 'label',
180 'label' => esc_html__('Label', 'king-addons'),
181 'type' => Controls_Manager::TEXT,
182 ],
183 [
184 'name' => 'help',
185 'label' => esc_html__('Help text (hint)', 'king-addons'),
186 'type' => Controls_Manager::TEXT,
187 ],
188 [
189 'name' => 'placeholder',
190 'label' => esc_html__('Placeholder', 'king-addons'),
191 'type' => Controls_Manager::TEXT,
192 ],
193 [
194 'name' => 'required',
195 'label' => esc_html__('Required', 'king-addons'),
196 'type' => Controls_Manager::SWITCHER,
197 'return_value' => 'yes',
198 ],
199 [
200 'name' => 'hide',
201 'label' => esc_html__('Hide field', 'king-addons'),
202 'type' => Controls_Manager::SWITCHER,
203 'return_value' => 'yes',
204 ],
205 [
206 'name' => 'priority',
207 'label' => esc_html__('Priority (order)', 'king-addons'),
208 'type' => Controls_Manager::NUMBER,
209 'default' => 20,
210 ],
211 [
212 'name' => 'visibility_mode',
213 'label' => esc_html__('Visibility rule', 'king-addons'),
214 'type' => Controls_Manager::SELECT,
215 'options' => [
216 'all' => esc_html__('Show everywhere', 'king-addons'),
217 'show' => esc_html__('Show only for countries', 'king-addons'),
218 'hide' => esc_html__('Hide for countries', 'king-addons'),
219 ],
220 'default' => 'all',
221 ],
222 [
223 'name' => 'countries',
224 'label' => esc_html__('Countries (Pro)', 'king-addons'),
225 'type' => Controls_Manager::SELECT2,
226 'multiple' => true,
227 'options' => (function_exists('WC') && WC()->countries) ? WC()->countries->get_countries() : [],
228 ],
229 ],
230 'default' => [],
231 'title_field' => '{{ field_key }}',
232 ]
233 );
234
235 $this->add_control(
236 'extra_fields',
237 [
238 'label' => esc_html__('Extra fields (Pro)', 'king-addons'),
239 'type' => Controls_Manager::REPEATER,
240 'condition' => [
241 'enable_field_customization' => 'yes',
242 ],
243 'fields' => [
244 [
245 'name' => 'field_key',
246 'label' => esc_html__('Field key', 'king-addons'),
247 'type' => Controls_Manager::TEXT,
248 'placeholder' => 'order_reference',
249 ],
250 [
251 'name' => 'section',
252 'label' => esc_html__('Section', 'king-addons'),
253 'type' => Controls_Manager::SELECT,
254 'options' => [
255 'billing' => esc_html__('Billing', 'king-addons'),
256 'shipping' => esc_html__('Shipping', 'king-addons'),
257 'order' => esc_html__('Order notes', 'king-addons'),
258 ],
259 'default' => 'order',
260 ],
261 [
262 'name' => 'label',
263 'label' => esc_html__('Label', 'king-addons'),
264 'type' => Controls_Manager::TEXT,
265 'default' => esc_html__('Extra field', 'king-addons'),
266 ],
267 [
268 'name' => 'placeholder',
269 'label' => esc_html__('Placeholder', 'king-addons'),
270 'type' => Controls_Manager::TEXT,
271 ],
272 [
273 'name' => 'type',
274 'label' => esc_html__('Type', 'king-addons'),
275 'type' => Controls_Manager::SELECT,
276 'options' => [
277 'text' => esc_html__('Text', 'king-addons'),
278 'textarea' => esc_html__('Textarea', 'king-addons'),
279 ],
280 'default' => 'text',
281 ],
282 [
283 'name' => 'required',
284 'label' => esc_html__('Required', 'king-addons'),
285 'type' => Controls_Manager::SWITCHER,
286 'return_value' => 'yes',
287 ],
288 [
289 'name' => 'priority',
290 'label' => esc_html__('Priority (order)', 'king-addons'),
291 'type' => Controls_Manager::NUMBER,
292 'default' => 80,
293 ],
294 ],
295 'default' => [],
296 'title_field' => '{{ field_key }}',
297 ]
298 );
299
300 $this->end_controls_section();
301
302 $this->start_controls_section(
303 'section_style',
304 [
305 'label' => esc_html__('Form', 'king-addons'),
306 'tab' => Controls_Manager::TAB_STYLE,
307 ]
308 );
309
310 $this->add_group_control(
311 Group_Control_Typography::get_type(),
312 [
313 'name' => 'form_typography',
314 'selector' => '{{WRAPPER}} .woocommerce',
315 ]
316 );
317
318 $this->add_control(
319 'text_color',
320 [
321 'label' => esc_html__('Text Color', 'king-addons'),
322 'type' => Controls_Manager::COLOR,
323 'selectors' => [
324 '{{WRAPPER}} .woocommerce' => 'color: {{VALUE}};',
325 ],
326 ]
327 );
328
329 $this->add_group_control(
330 Group_Control_Border::get_type(),
331 [
332 'name' => 'form_border',
333 'selector' => '{{WRAPPER}} .woocommerce form.checkout',
334 ]
335 );
336
337 $this->add_group_control(
338 Group_Control_Box_Shadow::get_type(),
339 [
340 'name' => 'form_shadow',
341 'selector' => '{{WRAPPER}} .woocommerce form.checkout',
342 ]
343 );
344
345 $this->add_control(
346 'form_padding',
347 [
348 'label' => esc_html__('Form Padding', 'king-addons'),
349 'type' => Controls_Manager::DIMENSIONS,
350 'size_units' => ['px', 'em', '%'],
351 'selectors' => [
352 '{{WRAPPER}} .woocommerce form.checkout' => 'padding: {{TOP}}{{UNIT}} {{RIGHT}}{{UNIT}} {{BOTTOM}}{{UNIT}} {{LEFT}}{{UNIT}};',
353 ],
354 ]
355 );
356
357 $this->end_controls_section();
358 }
359
360 /**
361 * Render widget output.
362 *
363 * @return void
364 */
365 protected function render(): void
366 {
367 if (!$this->should_render()) {
368 $this->render_missing_checkout_notice();
369 return;
370 }
371
372 if (!function_exists('WC')) {
373 return;
374 }
375
376 $settings = $this->get_settings_for_display();
377 $can_pro = king_addons_can_use_pro();
378 self::$is_rendering = true;
379
380 $show_login = !empty($settings['show_login']);
381 $show_coupon = !empty($settings['show_coupon']);
382 $show_notes = !empty($settings['show_order_notes']);
383 $field_customization = $can_pro && !empty($settings['enable_field_customization']);
384
385 $removed = [];
386
387 if (!$can_pro) {
388 $show_login = true;
389 $show_coupon = true;
390 $show_notes = true;
391 }
392
393 // Dedicated widgets already print these blocks; keep one copy.
394 if (Woo_Context::template_has_widget('woo_checkout_login', 'checkout')) {
395 $show_login = false;
396 }
397 if (Woo_Context::template_has_widget('woo_checkout_coupon', 'checkout')) {
398 $show_coupon = false;
399 }
400
401 if (!$show_login) {
402 remove_action('woocommerce_before_checkout_form', 'woocommerce_checkout_login_form', 10);
403 $removed[] = 'login';
404 }
405
406 if (!$show_coupon) {
407 remove_action('woocommerce_before_checkout_form', 'woocommerce_checkout_coupon_form', 10);
408 $removed[] = 'coupon';
409 }
410
411 $hide_native_payment = Woo_Context::template_has_widget('woo_checkout_payment', 'checkout');
412 if ($hide_native_payment) {
413 remove_action('woocommerce_checkout_order_review', 'woocommerce_checkout_payment', 20);
414 }
415
416 $notes_filter_added = false;
417 if (!$show_notes) {
418 add_filter('woocommerce_enable_order_notes_field', '__return_false', 9999);
419 $notes_filter_added = true;
420 }
421
422 $fields_filter_added = false;
423 if ($field_customization) {
424 $fields_filter_added = true;
425 self::$cached_config = $settings['fields_config'] ?? [];
426 self::$cached_extra = $settings['extra_fields'] ?? [];
427 $this->add_render_attribute(
428 '_wrapper',
429 'data-ka-fields-config',
430 wp_json_encode(self::$cached_config)
431 );
432 add_filter('woocommerce_checkout_fields', [self::class, 'filter_checkout_fields'], 9999);
433 add_action('woocommerce_checkout_update_order_meta', [self::class, 'save_extra_fields'], 20, 2);
434 // The order is created by a separate ?wc-ajax=checkout request in
435 // which no Elementor widget renders, so hooks added here are gone
436 // by then - extra fields were shown, filled in and silently
437 // dropped. Stash the configuration for that request to pick up.
438 self::remember_config(self::$cached_config, self::$cached_extra);
439 self::$remembered_this_request = true;
440 } elseif (!self::$remembered_this_request) {
441 // Only drop a stored configuration when nothing on this page set
442 // one: a second, plainer checkout widget must not wipe what the
443 // customised one just saved for the submit request.
444 self::forget_config();
445 }
446
447 // WooCommerce has no woocommerce_checkout() function - the guard was
448 // always false and the widget printed nothing at all. The checkout form
449 // comes from the shortcode handler, which also covers the pay-for-order
450 // and order-received states.
451 // Style selectors target {{WRAPPER}} .woocommerce form.checkout.
452 // WC_Shortcode_Checkout::output() prints the form without that wrap
453 // on the checkout page, so padding/color never applied.
454 echo '<div class="woocommerce">';
455 if (class_exists('WC_Shortcode_Checkout')) {
456 \WC_Shortcode_Checkout::output([]);
457 } elseif (function_exists('woocommerce_checkout')) {
458 call_user_func('woocommerce_checkout');
459 }
460 echo '</div>';
461
462 if (in_array('login', $removed, true)) {
463 add_action('woocommerce_before_checkout_form', 'woocommerce_checkout_login_form', 10);
464 }
465 if (in_array('coupon', $removed, true)) {
466 add_action('woocommerce_before_checkout_form', 'woocommerce_checkout_coupon_form', 10);
467 }
468 if ($hide_native_payment) {
469 add_action('woocommerce_checkout_order_review', 'woocommerce_checkout_payment', 20);
470 }
471 if ($notes_filter_added) {
472 remove_filter('woocommerce_enable_order_notes_field', '__return_false', 9999);
473 }
474 if ($fields_filter_added) {
475 remove_filter('woocommerce_checkout_fields', [self::class, 'filter_checkout_fields'], 9999);
476 remove_action('woocommerce_checkout_update_order_meta', [self::class, 'save_extra_fields'], 20);
477 self::$cached_config = [];
478 self::$cached_extra = [];
479 }
480
481 self::$is_rendering = false;
482 }
483
484 /**
485 * Session key holding the field configuration for the submit request.
486 */
487 private const SESSION_KEY = 'king_addons_checkout_fields';
488
489 /**
490 * True while a checkout form widget is rendering.
491 *
492 * @var bool
493 */
494 private static $is_rendering = false;
495
496 /**
497 * True once a widget in this request stored a field configuration.
498 *
499 * @var bool
500 */
501 private static $remembered_this_request = false;
502
503 /**
504 * Store the configuration for the request that actually creates the order.
505 *
506 * @param array<int,array<string,mixed>> $config Existing-field settings.
507 * @param array<int,array<string,mixed>> $extra Extra field definitions.
508 *
509 * @return void
510 */
511 private static function remember_config(array $config, array $extra): void
512 {
513 if (!function_exists('WC') || !WC()->session) {
514 return;
515 }
516
517 WC()->session->set(self::SESSION_KEY, ['config' => $config, 'extra' => $extra]);
518 }
519
520 /**
521 * Drop a previously stored configuration.
522 *
523 * @return void
524 */
525 private static function forget_config(): void
526 {
527 if (!function_exists('WC') || !WC()->session) {
528 return;
529 }
530
531 if (WC()->session->get(self::SESSION_KEY)) {
532 WC()->session->set(self::SESSION_KEY, null);
533 }
534 }
535
536 /**
537 * Read the stored configuration.
538 *
539 * @return array{config: array<int,array<string,mixed>>, extra: array<int,array<string,mixed>>}|null
540 */
541 private static function stored_config(): ?array
542 {
543 if (!function_exists('WC') || !WC()->session) {
544 return null;
545 }
546
547 $stored = WC()->session->get(self::SESSION_KEY);
548 if (!is_array($stored)) {
549 return null;
550 }
551
552 return [
553 'config' => isset($stored['config']) && is_array($stored['config']) ? $stored['config'] : [],
554 'extra' => isset($stored['extra']) && is_array($stored['extra']) ? $stored['extra'] : [],
555 ];
556 }
557
558 /**
559 * Hook the checkout-field handling for requests that never render a widget.
560 *
561 * Registered once at plugin load; both callbacks do nothing unless the
562 * checkout form widget stored a configuration for this visitor.
563 *
564 * @return void
565 */
566 public static function register_persistent_hooks(): void
567 {
568 add_filter('woocommerce_checkout_fields', [self::class, 'filter_stored_checkout_fields'], 9998);
569 add_action('woocommerce_checkout_update_order_meta', [self::class, 'save_stored_extra_fields'], 20, 2);
570 add_action('woocommerce_admin_order_data_after_billing_address', [self::class, 'render_extra_fields_admin']);
571 add_action('woocommerce_order_details_after_order_table', [self::class, 'render_extra_fields_front']);
572 add_filter('woocommerce_email_order_meta_fields', [self::class, 'email_extra_fields'], 10, 3);
573 }
574
575 /**
576 * Apply the stored field configuration.
577 *
578 * @param array<string,array<string,array<string,mixed>>> $fields WC checkout fields.
579 *
580 * @return array<string,array<string,array<string,mixed>>>
581 */
582 public static function filter_stored_checkout_fields(array $fields): array
583 {
584 // A widget on the page owns its own settings - including the choice not
585 // to customise anything. Applying the stored copy during any render
586 // would leak one widget's field setup into its neighbours.
587 if (self::$is_rendering) {
588 return $fields;
589 }
590
591 $stored = self::stored_config();
592 if (null === $stored) {
593 return $fields;
594 }
595
596 return self::tune_checkout_fields($fields, $stored['config'], $stored['extra']);
597 }
598
599 /**
600 * Save extra fields using the stored configuration.
601 *
602 * @param int $order_id Order ID.
603 * @param array $data Posted data.
604 *
605 * @return void
606 */
607 public static function save_stored_extra_fields(int $order_id, array $data): void
608 {
609 if (self::$is_rendering || !empty(self::$cached_extra)) {
610 return;
611 }
612
613 $stored = self::stored_config();
614 if (null === $stored || empty($stored['extra'])) {
615 return;
616 }
617
618 self::$cached_extra = $stored['extra'];
619 self::save_extra_fields($order_id, $data);
620 self::$cached_extra = [];
621 }
622
623 /**
624 * Wrapper for WC checkout fields filter using cached config.
625 *
626 * @param array<string,array<string,array<string,mixed>>> $fields WC checkout fields.
627 *
628 * @return array<string,array<string,array<string,mixed>>>
629 */
630 public static function filter_checkout_fields(array $fields): array
631 {
632 return self::tune_checkout_fields($fields, self::$cached_config, self::$cached_extra);
633 }
634
635 /**
636 * Adjust checkout fields based on widget settings (Pro).
637 *
638 * @param array<string,array<string,array<string,mixed>>> $fields WC checkout fields.
639 * @param array<int,array<string,mixed>> $config Configured existing fields.
640 * @param array<int,array<string,mixed>> $extra Extra fields to inject.
641 *
642 * @return array<string,array<string,array<string,mixed>>>
643 */
644 public static function tune_checkout_fields(array $fields, array $config, array $extra): array
645 {
646 foreach ($config as $item) {
647 $section = $item['section'] ?? '';
648 $key = $item['field_key'] ?? '';
649 if (empty($section) || empty($key) || empty($fields[$section][$key])) {
650 continue;
651 }
652 // Country-based visibility.
653 $mode = $item['visibility_mode'] ?? 'all';
654 $countries = !empty($item['countries']) && is_array($item['countries']) ? array_filter(array_map('sanitize_text_field', $item['countries'])) : [];
655 if ('all' !== $mode && !empty($countries) && function_exists('WC')) {
656 $customer = WC()->customer;
657 $current_country = $customer ? $customer->get_shipping_country() : '';
658 if (empty($current_country) && $customer) {
659 $current_country = $customer->get_billing_country();
660 }
661 if ('show' === $mode && !in_array($current_country, $countries, true)) {
662 unset($fields[$section][$key]);
663 continue;
664 }
665 if ('hide' === $mode && in_array($current_country, $countries, true)) {
666 unset($fields[$section][$key]);
667 continue;
668 }
669 }
670 if (!empty($item['hide']) && 'yes' === $item['hide']) {
671 unset($fields[$section][$key]);
672 continue;
673 }
674 if (isset($item['label']) && $item['label'] !== '') {
675 $fields[$section][$key]['label'] = sanitize_text_field($item['label']);
676 }
677 if (isset($item['placeholder']) && $item['placeholder'] !== '') {
678 $fields[$section][$key]['placeholder'] = sanitize_text_field($item['placeholder']);
679 } elseif (empty($fields[$section][$key]['placeholder']) && !empty($fields[$section][$key]['label'])) {
680 $fields[$section][$key]['placeholder'] = $fields[$section][$key]['label'];
681 }
682 if (isset($item['help']) && $item['help'] !== '') {
683 $fields[$section][$key]['description'] = sanitize_text_field($item['help']);
684 }
685 if (isset($item['required'])) {
686 $fields[$section][$key]['required'] = ('yes' === $item['required']);
687 }
688 if (isset($item['priority']) && '' !== $item['priority']) {
689 $fields[$section][$key]['priority'] = (int) $item['priority'];
690 }
691 }
692
693 foreach ($extra as $item) {
694 $section = $item['section'] ?? 'order';
695 $key = $item['field_key'] ?? '';
696 if (empty($key)) {
697 continue;
698 }
699 $type = ('textarea' === ($item['type'] ?? 'text')) ? 'textarea' : 'text';
700 $label = isset($item['label']) ? sanitize_text_field($item['label']) : '';
701 $placeholder = isset($item['placeholder']) ? sanitize_text_field($item['placeholder']) : '';
702 $required = ('yes' === ($item['required'] ?? ''));
703 $priority = isset($item['priority']) ? (int) $item['priority'] : 80;
704
705 $fields[$section][$key] = [
706 'type' => $type,
707 'label' => $label,
708 'placeholder' => $placeholder,
709 'required' => $required,
710 'priority' => $priority,
711 ];
712 }
713
714 return $fields;
715 }
716
717 /**
718 * Save extra checkout fields to order meta.
719 *
720 * @param int $order_id Order ID.
721 * @param array $data Posted data.
722 *
723 * @return void
724 */
725 public static function save_extra_fields(int $order_id, array $data): void
726 {
727 $order = wc_get_order($order_id);
728 if (!$order) {
729 return;
730 }
731
732 $posted = $_POST ?? []; // phpcs:ignore WordPress.Security.NonceVerification.Missing
733
734 $types = [];
735 $labels = [];
736 foreach (self::$cached_extra as $item) {
737 if (!empty($item['field_key'])) {
738 $types[$item['field_key']] = $item['type'] ?? 'text';
739 $labels[$item['field_key']] = (string) ($item['label'] ?? $item['field_key']);
740 }
741 }
742
743 foreach ($types as $extra_key => $type) {
744 if (!isset($posted[$extra_key])) {
745 continue;
746 }
747 $raw = $posted[$extra_key];
748 if (is_array($raw)) {
749 $clean = implode(', ', array_map('sanitize_text_field', $raw));
750 } else {
751 $clean = ('textarea' === $type) ? sanitize_textarea_field((string) $raw) : sanitize_text_field((string) $raw);
752 }
753 $order->update_meta_data('_ka_' . sanitize_key((string) $extra_key), $clean);
754 }
755 if (!empty($labels)) {
756 $order->update_meta_data('_ka_extra_labels', $labels);
757 }
758 $order->save();
759 }
760
761 /**
762 * Extra checkout fields saved on the order, as key => [label, value].
763 *
764 * @param \WC_Order $order Order.
765 *
766 * @return array<string,array{label:string,value:string}>
767 */
768 public static function extra_fields_from_order($order): array
769 {
770 if (!$order instanceof \WC_Order) {
771 return [];
772 }
773
774 $labels = $order->get_meta('_ka_extra_labels');
775 if (!is_array($labels) || empty($labels)) {
776 return [];
777 }
778
779 $out = [];
780 foreach ($labels as $key => $label) {
781 $value = $order->get_meta('_ka_' . sanitize_key((string) $key));
782 if ('' === (string) $value) {
783 continue;
784 }
785 $out[(string) $key] = [
786 'label' => sanitize_text_field((string) $label),
787 'value' => sanitize_text_field((string) $value),
788 ];
789 }
790
791 return $out;
792 }
793
794 /**
795 * Show extra fields in wp-admin order screen.
796 *
797 * @param \WC_Order $order Order.
798 *
799 * @return void
800 */
801 public static function render_extra_fields_admin($order): void
802 {
803 $fields = self::extra_fields_from_order($order);
804 if (empty($fields)) {
805 return;
806 }
807
808 echo '<div class="ka-order-extra-fields">';
809 echo '<h3>' . esc_html__('Extra fields', 'king-addons') . '</h3>';
810 foreach ($fields as $field) {
811 echo '<p><strong>' . esc_html($field['label']) . ':</strong> ' . esc_html($field['value']) . '</p>';
812 }
813 echo '</div>';
814 }
815
816 /**
817 * Show extra fields on the thank-you / view-order screens.
818 *
819 * @param \WC_Order $order Order.
820 *
821 * @return void
822 */
823 public static function render_extra_fields_front($order): void
824 {
825 $fields = self::extra_fields_from_order($order);
826 if (empty($fields)) {
827 return;
828 }
829
830 echo '<section class="ka-order-extra-fields">';
831 echo '<h2>' . esc_html__('Extra fields', 'king-addons') . '</h2>';
832 echo '<table class="woocommerce-table shop_table"><tbody>';
833 foreach ($fields as $field) {
834 echo '<tr><th>' . esc_html($field['label']) . '</th><td>' . esc_html($field['value']) . '</td></tr>';
835 }
836 echo '</tbody></table></section>';
837 }
838
839 /**
840 * Extra fields in WooCommerce emails. Values are plain text on purpose:
841 * this filter is printed raw in both HTML and plain-text mail.
842 *
843 * @param array<string,array<string,string>> $fields Existing meta fields.
844 * @param bool $sent_to_admin Unused.
845 * @param \WC_Order $order Order.
846 *
847 * @return array<string,array<string,string>>
848 */
849 public static function email_extra_fields($fields, $sent_to_admin, $order): array
850 {
851 unset($sent_to_admin);
852 if (!is_array($fields)) {
853 $fields = [];
854 }
855
856 foreach (self::extra_fields_from_order($order) as $key => $field) {
857 $fields['ka_' . sanitize_key((string) $key)] = [
858 'label' => $field['label'],
859 'value' => $field['value'],
860 ];
861 }
862
863 return $fields;
864 }
865 }
866
867
868
869
870
871
872