PluginProbe
WPFunnels – Funnel Builder for WooCommerce with Checkout & One Click Upsell / 3.12.9
WPFunnels – Funnel Builder for WooCommerce with Checkout & One Click Upsell v3.12.9
3.13.3 3.13.2 3.13.1 3.13.0 3.12.13 3.12.12 3.12.11 3.12.10 3.12.9 3.12.8 3.12.7 3.12.6 3.12.5 3.12.4 3.12.3 3.12.1 3.12.2 3.12.0 3.11.1 3.11.0 3.10.9 3.10.8 3.10.7 3.10.6 2.8.16 All 261 releases
wpfunnels / includes / core / rest-api / Controllers / class-funnel-controller.php

class-funnel-controller.php in WPFunnels – Funnel Builder for WooCommerce with Checkout & One Click Upsell 3.12.9, at includes/core/rest-api/Controllers/class-funnel-controller.php

2,098 lines 64.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Funnel controller
4 *
5 * @package WPFunnels\Rest\Controllers
6 */
7
8 namespace WPFunnels\Rest\Controllers;
9
10 use Error;
11 use WP_Error;
12 use WP_REST_Request;
13 use WP_REST_Response;
14 use WPFunnels\Wpfnl_functions;
15 use Wpfnl_Controller_Type_Factory;
16 use WPFunnels\Modules\Admin\Funnel\Module;
17 use WPFunnels\Wpfnl;
18 use WPFunnels\Migration\Migration;
19
20 class FunnelController extends Wpfnl_REST_Controller
21 {
22
23 /**
24 * Endpoint namespace.
25 *
26 * @var string
27 */
28 protected $namespace = 'wpfunnels/v1';
29
30 /**
31 * Route base.
32 *
33 * @var string
34 */
35 protected $rest_base = 'funnel-control';
36
37 /**
38 * Check if user has valid permission
39 *
40 * @param $request
41 *
42 * @return bool|WP_Error
43 * @since 1.0.0
44 */
45 public function update_items_permissions_check($request)
46 {
47 if (!Wpfnl_functions::wpfnl_rest_check_manager_permissions('steps', 'edit')) {
48 return new WP_Error('wpfunnels_rest_cannot_edit', __('Sorry, you cannot edit this resource.', 'wpfnl'), ['status' => rest_authorization_required_code()]);
49 }
50 return true;
51 }
52
53 /**
54 * Makes sure the current user has access to READ the settings APIs.
55 *
56 * @param WP_REST_Request $request Full data about the request.
57 *
58 * @return WP_Error|boolean
59 * @since 3.0.0
60 */
61 public function get_items_permissions_check($request)
62 {
63 if (!Wpfnl_functions::wpfnl_rest_check_manager_permissions('settings')) {
64 return new WP_Error('wpfunnels_rest_cannot_view', __('Sorry, you cannot list resources.', 'wpfnl'), ['status' => rest_authorization_required_code()]);
65 }
66 return true;
67 }
68
69
70 /**
71 * Register rest routes
72 *
73 * @since 1.0.0
74 */
75 public function register_routes()
76 {
77 register_rest_route($this->namespace, '/' . $this->rest_base . '/saveFunnel/', [
78 [
79 'methods' => \WP_REST_Server::EDITABLE,
80 'callback' => [
81 $this,
82 'save_funnel_data'
83 ],
84 'permission_callback' => [
85 $this,
86 'update_items_permissions_check'
87 ],
88 ],
89 ]);
90
91 register_rest_route($this->namespace, '/' . $this->rest_base . '/getThankyouData/', [
92 [
93 'methods' => \WP_REST_Server::READABLE,
94 'callback' => [
95 $this,
96 'get_thankyou_data'
97 ],
98 'permission_callback' => [
99 $this,
100 'update_items_permissions_check'
101 ],
102 ],
103 ]);
104
105 register_rest_route($this->namespace, '/' . $this->rest_base . '/saveConditionalNode/', [
106 [
107 'methods' => \WP_REST_Server::EDITABLE,
108 'callback' => [
109 $this,
110 'save_conditional_node'
111 ],
112 'permission_callback' => [
113 $this,
114 'update_items_permissions_check'
115 ],
116 ],
117 ]);
118 register_rest_route($this->namespace, '/' . $this->rest_base . '/getConditionalNode/', [
119 [
120 'methods' => \WP_REST_Server::READABLE,
121 'callback' => [
122 $this,
123 'get_conditional_node'
124 ],
125 'permission_callback' => [
126 $this,
127 'get_items_permissions_check'
128 ],
129 ],
130 ]);
131 register_rest_route($this->namespace, '/' . $this->rest_base . '/getFunnel/', [
132 [
133 'methods' => \WP_REST_Server::READABLE,
134 'callback' => [
135 $this,
136 'get_funnel_data'
137 ],
138 'permission_callback' => [
139 $this,
140 'update_items_permissions_check'
141 ],
142 ],
143 ]);
144
145 register_rest_route($this->namespace, '/' . $this->rest_base . '/getStepType/', [
146 [
147 'methods' => \WP_REST_Server::READABLE,
148 'callback' => [
149 $this,
150 'get_step_type'
151 ],
152 'permission_callback' => [
153 $this,
154 'update_items_permissions_check'
155 ],
156 ],
157 ]);
158
159 register_rest_route($this->namespace, '/' . $this->rest_base . '/getFunnelInfo/', [
160 [
161 'methods' => \WP_REST_Server::READABLE,
162 'callback' => [
163 $this,
164 'get_funnel_info'
165 ],
166 'permission_callback' => [
167 $this,
168 'update_items_permissions_check'
169 ],
170 ],
171 ]);
172
173
174 register_rest_route($this->namespace, '/' . $this->rest_base . '/exportFunnel/', [
175 [
176 'methods' => \WP_REST_Server::READABLE,
177 'callback' => [
178 $this,
179 'export_funnel'
180 ],
181 'permission_callback' => [
182 $this,
183 'update_items_permissions_check'
184 ],
185 ],
186 ]);
187
188 register_rest_route($this->namespace, '/' . $this->rest_base . '/getallfunnels/', array(
189 array(
190 'methods' => \WP_REST_Server::READABLE,
191 'callback' => [
192 $this,
193 'get_all_funnels'
194 ],
195 'permission_callback' => [
196 $this,
197 'update_items_permissions_check'
198 ],
199 ),
200 ));
201
202 register_rest_route($this->namespace, '/' . $this->rest_base . '/getStepGeneralInfo/', [
203 [
204 'methods' => \WP_REST_Server::READABLE,
205 'callback' => [
206 $this,
207 'get_step_general_info'
208 ],
209 'permission_callback' => [
210 $this,
211 'update_items_permissions_check'
212 ],
213 ],
214 ]);
215
216 register_rest_route($this->namespace, '/' . $this->rest_base . '/steps/', array(
217 'args' => array(
218 'funnel_id' => array(
219 'description' => __('Funnel ID.', 'wpfnl'),
220 'type' => 'string',
221 ),
222 'step_id' => array(
223 'description' => __('Step ID.', 'wpfnl'),
224 'type' => 'string',
225 )
226 ),
227 array(
228 'methods' => \WP_REST_Server::EDITABLE,
229 'callback' => [
230 $this,
231 'update_step_meta'
232 ],
233 'permission_callback' => [
234 $this,
235 'update_items_permissions_check'
236 ],
237 )
238 ));
239
240
241 register_rest_route($this->namespace, '/' . $this->rest_base . '/get_gbf_data/(?P<funnel_id>\d+)', [
242 [
243 'methods' => \WP_REST_Server::READABLE,
244 'callback' => [
245 $this,
246 'get_GBF_data'
247 ],
248 'permission_callback' => [
249 $this,
250 'update_items_permissions_check'
251 ],
252 ],
253 ]);
254
255 register_rest_route($this->namespace, '/' . $this->rest_base . '/get-optimizations/(?P<step_id>[\d]+)', [
256 [
257 'methods' => \WP_REST_Server::READABLE,
258 'callback' => [
259 $this,
260 'get_optimizations'
261 ],
262 'permission_callback' => [
263 $this,
264 'update_items_permissions_check'
265 ],
266 ],
267 ]);
268
269 register_rest_route($this->namespace, '/' . $this->rest_base . '/save-optimizations/', [
270 [
271 'methods' => \WP_REST_Server::EDITABLE,
272 'callback' => [
273 $this,
274 'save_optimizations'
275 ],
276 'permission_callback' => [
277 $this,
278 'update_items_permissions_check'
279 ],
280 ],
281 ]);
282
283 register_rest_route($this->namespace, '/' . $this->rest_base . '/get-settings/(?P<step_id>[\d]+)', [
284 [
285 'methods' => \WP_REST_Server::READABLE,
286 'callback' => [
287 $this,
288 'get_ab_settings'
289 ],
290 'permission_callback' => [
291 $this,
292 'update_items_permissions_check'
293 ],
294 ],
295 ]);
296
297 register_rest_route(
298 $this->namespace,
299 '/' . $this->rest_base . '/(?P<funnel_id>\d+)/funnel-name-change',
300 array(
301 array(
302 'methods' => \WP_REST_Server::EDITABLE,
303 'callback' => array(
304 $this,
305 'funnel_name_change',
306 ),
307 'permission_callback' => array(
308 $this,
309 'update_items_permissions_check',
310 ),
311 ),
312 )
313 );
314
315 register_rest_route(
316 $this->namespace,
317 '/' . $this->rest_base . '/wpfnl-get-funnel-settings',
318 array(
319 array(
320 'methods' => \WP_REST_Server::EDITABLE,
321 'callback' => array(
322 $this,
323 'wpfnl_get_funnel_settings',
324 ),
325 'args' => array(
326 'funnel_id' => array(
327 'required' => true,
328 'type' => array('integer', 'string'),
329 'sanitize_callback' => 'absint',
330 ),
331 ),
332 'permission_callback' => array(
333 $this,
334 'update_items_permissions_check',
335 )
336 ),
337 )
338 );
339
340 register_rest_route(
341 $this->namespace,
342 '/' . $this->rest_base . '/bulk-delete-funnel',
343 array(
344 array(
345 'methods' => \WP_REST_Server::EDITABLE,
346 'callback' => array(
347 $this,
348 'delete_marked_funnels',
349 ),
350 'permission_callback' => array(
351 $this,
352 'update_items_permissions_check',
353 ),
354 ),
355 )
356 );
357
358 register_rest_route(
359 $this->namespace,
360 '/' . $this->rest_base . '/bulk-restore-funnel',
361 array(
362 array(
363 'methods' => \WP_REST_Server::EDITABLE,
364 'callback' => array(
365 $this,
366 'restore_marked_funnels',
367 ),
368 'permission_callback' => array(
369 $this,
370 'update_items_permissions_check',
371 ),
372 ),
373 )
374 );
375
376 register_rest_route(
377 $this->namespace,
378 '/' . $this->rest_base . '/bulk-trash-funnel',
379 array(
380 array(
381 'methods' => \WP_REST_Server::EDITABLE,
382 'callback' => array(
383 $this,
384 'trash_marked_funnels',
385 ),
386 'permission_callback' => array(
387 $this,
388 'update_items_permissions_check',
389 ),
390 ),
391 )
392 );
393
394 register_rest_route(
395 $this->namespace,
396 '/' . $this->rest_base . '/(?P<funnel_id>\d+)/convert-to-store-checkout',
397 array(
398 array(
399 'methods' => \WP_REST_Server::EDITABLE,
400 'callback' => array( $this, 'convert_to_store_checkout' ),
401 'permission_callback' => array( $this, 'update_items_permissions_check' ),
402 'args' => array(
403 'funnel_id' => array(
404 'required' => true,
405 'validate_callback' => function ( $param ) {
406 return is_numeric( $param );
407 },
408 ),
409 ),
410 ),
411 )
412 );
413
414 }
415
416 /**
417 * Check if funnel data exists or not
418 **/
419 public function get_all_funnels()
420 {
421 $args = array(
422 'post_type' => 'wpfunnel_steps',
423 'numberposts' => -1
424 );
425 $funnels = get_posts($args);
426
427 if ($funnels) {
428 if (count($funnels) > 0) {
429 return false;
430 }
431 }
432
433 return true;
434 }
435
436
437 /**
438 * Save thankyou data
439 */
440 public function get_thankyou_data($request)
441 {
442 $step_id = $request['step_id'];
443 $data = array();
444 $data['_wpfnl_thankyou_order_overview'] = get_post_meta($step_id, '_wpfnl_thankyou_order_overview', true) ? get_post_meta($step_id, '_wpfnl_thankyou_order_overview', true) : 'on';
445 $data['_wpfnl_thankyou_order_details'] = get_post_meta($step_id, '_wpfnl_thankyou_order_details', true) ? get_post_meta($step_id, '_wpfnl_thankyou_order_details', true) : 'on';
446 $data['_wpfnl_thankyou_billing_details'] = get_post_meta($step_id, '_wpfnl_thankyou_billing_details', true) ? get_post_meta($step_id, '_wpfnl_thankyou_billing_details', true) : 'on';
447 $data['_wpfnl_thankyou_shipping_details'] = get_post_meta($step_id, '_wpfnl_thankyou_shipping_details', true) ? get_post_meta($step_id, '_wpfnl_thankyou_shipping_details', true) : 'on';
448 $data['_wpfnl_thankyou_is_custom_redirect'] = get_post_meta($step_id, '_wpfnl_thankyou_is_custom_redirect', true) ? get_post_meta($step_id, '_wpfnl_thankyou_is_custom_redirect', true) : 'off';
449 $data['_wpfnl_thankyou_is_direct_redirect'] = get_post_meta($step_id, '_wpfnl_thankyou_is_direct_redirect', true) ? get_post_meta($step_id, '_wpfnl_thankyou_is_direct_redirect', true) : 'off';
450 $data['_wpfnl_thankyou_set_time'] = get_post_meta($step_id, '_wpfnl_thankyou_set_time', true) ? get_post_meta($step_id, '_wpfnl_thankyou_set_time', true) : '';
451 $data['_wpfnl_thankyou_custom_redirect_url'] = get_post_meta($step_id, '_wpfnl_thankyou_custom_redirect_url', true) ? get_post_meta($step_id, '_wpfnl_thankyou_custom_redirect_url', true) : '';
452 return $data;
453 }
454
455 /**
456 * Get conditional node
457 *
458 * @param WP_REST_Request $request request.
459 *
460 * @return array|WP_Error
461 */
462 public function get_conditional_node($request)
463 {
464 $funnel_id = $request['funnel_id'];
465
466 $steps = Wpfnl_functions::get_steps($funnel_id);
467 $optin_step = [];
468 if (is_array($steps)) {
469 foreach ($steps as $step) {
470 if ('landing' === $step['step_type'] || 'custom' === $step['step_type']) {
471 $optin_step[] = [
472 'name' => __('Optin form (', 'wpfnl') . $step['name'] . ')',
473 'value' => __('optin_', 'wpfnl') . $step['id'],
474 ];
475 }
476 }
477 }
478 $response = array(
479 'status' => 'error',
480 );
481 if (!empty($optin_step)) {
482 $response['status'] = 'success';
483 $response['optinStep'] = $optin_step;
484 }
485
486 return $this->prepare_item_for_response($response, $request);
487 }
488
489 /**
490 * Save conditional node
491 *
492 * @param string $request request.
493 *
494 * @return array|WP_Error
495 */
496 public function save_conditional_node($request)
497 {
498 $funnel_id = $request['funnel_id'];
499 $condition_data = $request['condition_data'];
500 $node_identifier = $request['node_identifier'];
501 update_post_meta($funnel_id, $node_identifier, $condition_data);
502 $response = array(
503 'status' => true,
504 );
505 return $this->prepare_item_for_response($response, $request);
506 }
507
508 /**
509 * Get step_type.
510 *
511 * @param string $request request.
512 *
513 * @return array|WP_Error
514 */
515 public function get_step_type($request)
516 {
517 $step_type = '';
518 $step_id = $request['step_id'];
519 $step_type = get_post_meta($step_id, '_step_type', true);
520 return $step_type;
521 }
522
523
524 /**
525 * Retrieve checkout optimization settings for a funnel step.
526 *
527 * Reads post meta for the given step and returns optimization settings
528 * including express checkout, field validation, product images display,
529 * collapsible order summary, and enhanced phone field configuration.
530 * Falls back to default values when no meta is stored.
531 *
532 * @param WP_REST_Request $request Request object. Expects 'step_id' parameter.
533 * @return WP_REST_Response REST response containing the optimization settings.
534 */
535 public function get_optimizations($request) {
536 $step_id = $request['step_id'];
537 $express_checkout_enabled = get_post_meta($step_id, '_wpfnl_express_checkout_enabled', true);
538 $express_checkout_position = get_post_meta($step_id, '_wpfnl_express_checkout_position', true);
539 $field_validation_enabled = get_post_meta($step_id, '_wpfnl_field_validation_enabled', true);
540 $field_validation_message = get_post_meta($step_id, '_wpfnl_field_validation_message', true);
541 $display_product_images = get_post_meta($step_id, '_wpfnl_display_product_images', true);
542 $collapsible_order_summary_enabled = get_post_meta($step_id, '_wpfnl_collapsible_order_summary_enabled', true);
543 $enhanced_phone_field_enabled = get_post_meta($step_id, '_wpfnl_enhanced_phone_field_enabled', true);
544 $validate_phone_number = get_post_meta($step_id, '_wpfnl_validate_phone_number', true);
545 $save_phone_number_format = get_post_meta($step_id, '_wpfnl_save_phone_number_format', true);
546 $phone_help_text = get_post_meta($step_id, '_wpfnl_phone_help_text', true);
547
548 $testimonial_raw = get_post_meta($step_id, 'wpf_checkout_testimonial', true);
549 $testimonial_default = array(
550 'enabled' => false,
551 'layout' => 'layout-1',
552 'position' => 'after_bump',
553 'testimonial' => array( 'text' => '', 'author' => '', 'rating' => 5 ),
554 'guarantee' => array( 'headline' => '', 'text' => '', 'days' => 30, 'image' => array( 'id' => 0, 'url' => '' ) ),
555 'benefits' => array( 'title' => "Here's what you get", 'items' => array() ),
556 );
557 if ( is_array( $testimonial_raw ) ) {
558 $testimonial_config = wp_parse_args( $testimonial_raw, $testimonial_default );
559 // Deep-merge guarantee sub-array so new fields always exist
560 if ( ! isset( $testimonial_config['guarantee'] ) || ! is_array( $testimonial_config['guarantee'] ) ) {
561 $testimonial_config['guarantee'] = $testimonial_default['guarantee'];
562 } else {
563 $testimonial_config['guarantee'] = wp_parse_args( $testimonial_config['guarantee'], $testimonial_default['guarantee'] );
564 }
565 } else {
566 $testimonial_config = $testimonial_default;
567 }
568
569 $response = array(
570 'express_checkout_enabled' => $express_checkout_enabled ? $express_checkout_enabled : 'yes',
571 'express_checkout_position' => $express_checkout_position ? $express_checkout_position : 'top',
572 'field_validation_enabled' => $field_validation_enabled ? $field_validation_enabled : 'no',
573 'field_validation_message' => $field_validation_message ? $field_validation_message : '{field} is required',
574 'display_product_images' => $display_product_images ? $display_product_images : 'no',
575 'collapsible_order_summary_enabled' => $collapsible_order_summary_enabled ? $collapsible_order_summary_enabled : 'yes',
576 'enhanced_phone_field_enabled' => $enhanced_phone_field_enabled ? $enhanced_phone_field_enabled : 'no',
577 'validate_phone_number' => $validate_phone_number ? $validate_phone_number : 'no',
578 'save_phone_number_format' => $save_phone_number_format ? $save_phone_number_format : 'without_country_code',
579 'phone_help_text' => $phone_help_text ? $phone_help_text : '',
580 'testimonial' => $testimonial_config,
581 );
582 return $this->prepare_item_for_response($response, $request);
583 }
584
585 public function save_optimizations($request) {
586 $step_id = $request['step_id'];
587 $express_checkout_enabled = sanitize_text_field($request['express_checkout_enabled']);
588 $express_checkout_position = sanitize_text_field($request['express_checkout_position']);
589 update_post_meta($step_id, '_wpfnl_express_checkout_enabled', $express_checkout_enabled);
590 if ( $express_checkout_position ) {
591 update_post_meta($step_id, '_wpfnl_express_checkout_position', $express_checkout_position);
592 }
593
594 $field_validation_enabled = isset($request['field_validation_enabled']) ? sanitize_text_field($request['field_validation_enabled']) : 'no';
595 $field_validation_message = isset($request['field_validation_message']) ? sanitize_text_field($request['field_validation_message']) : '{field} is required';
596 update_post_meta($step_id, '_wpfnl_field_validation_enabled', $field_validation_enabled);
597 update_post_meta($step_id, '_wpfnl_field_validation_message', $field_validation_message);
598
599 $display_product_images = isset($request['display_product_images']) ? sanitize_text_field($request['display_product_images']) : 'no';
600 update_post_meta($step_id, '_wpfnl_display_product_images', $display_product_images);
601
602 $collapsible_order_summary_enabled = isset($request['collapsible_order_summary_enabled']) ? sanitize_text_field($request['collapsible_order_summary_enabled']) : 'yes';
603 update_post_meta($step_id, '_wpfnl_collapsible_order_summary_enabled', $collapsible_order_summary_enabled);
604
605 $enhanced_phone_field_enabled = isset($request['enhanced_phone_field_enabled']) ? sanitize_text_field($request['enhanced_phone_field_enabled']) : 'no';
606 $validate_phone_number = isset($request['validate_phone_number']) ? sanitize_text_field($request['validate_phone_number']) : 'no';
607 $save_phone_number_format = isset($request['save_phone_number_format']) ? sanitize_text_field($request['save_phone_number_format']) : 'without_country_code';
608 $phone_help_text = isset($request['phone_help_text']) ? sanitize_text_field($request['phone_help_text']) : '';
609
610 update_post_meta($step_id, '_wpfnl_enhanced_phone_field_enabled', $enhanced_phone_field_enabled);
611 update_post_meta($step_id, '_wpfnl_validate_phone_number', $validate_phone_number);
612 update_post_meta($step_id, '_wpfnl_save_phone_number_format', $save_phone_number_format);
613 update_post_meta($step_id, '_wpfnl_phone_help_text', $phone_help_text);
614
615 // Testimonial section
616 if ( isset( $request['testimonial'] ) && is_array( $request['testimonial'] ) ) {
617 $raw = $request['testimonial'];
618
619 $testimonial_data = array(
620 'enabled' => ! empty( $raw['enabled'] ),
621 'layout' => sanitize_key( isset( $raw['layout'] ) ? $raw['layout'] : 'layout-1' ),
622 'position' => sanitize_key( isset( $raw['position'] ) ? $raw['position'] : 'after_bump' ),
623 'testimonial' => array(
624 'text' => isset( $raw['testimonial']['text'] ) ? sanitize_textarea_field( $raw['testimonial']['text'] ) : '',
625 'author' => isset( $raw['testimonial']['author'] ) ? sanitize_text_field( $raw['testimonial']['author'] ) : '',
626 'rating' => isset( $raw['testimonial']['rating'] ) ? max( 1, min( 5, absint( $raw['testimonial']['rating'] ) ) ) : 5,
627 ),
628 'guarantee' => array(
629 'headline' => isset( $raw['guarantee']['headline'] ) ? sanitize_text_field( $raw['guarantee']['headline'] ) : '',
630 'text' => isset( $raw['guarantee']['text'] ) ? sanitize_textarea_field( $raw['guarantee']['text'] ) : '',
631 'days' => isset( $raw['guarantee']['days'] ) ? max( 1, absint( $raw['guarantee']['days'] ) ) : 30,
632 'image' => array(
633 'id' => isset( $raw['guarantee']['image']['id'] ) ? absint( $raw['guarantee']['image']['id'] ) : 0,
634 'url' => isset( $raw['guarantee']['image']['url'] ) ? esc_url_raw( $raw['guarantee']['image']['url'] ) : '',
635 ),
636 ),
637 'benefits' => array(
638 'title' => isset( $raw['benefits']['title'] ) ? sanitize_text_field( $raw['benefits']['title'] ) : '',
639 'items' => array(),
640 ),
641 );
642
643 if ( isset( $raw['benefits']['items'] ) && is_array( $raw['benefits']['items'] ) ) {
644 foreach ( $raw['benefits']['items'] as $item ) {
645 $testimonial_data['benefits']['items'][] = sanitize_text_field( $item );
646 }
647 }
648
649 update_post_meta( $step_id, 'wpf_checkout_testimonial', $testimonial_data );
650 }
651
652 $response = array(
653 'success' => true,
654 'message' => __('Saved Successfully', 'wpfnl')
655 );
656 return $this->prepare_item_for_response($response, $request);
657 }
658
659 /**
660 * Get step_type.
661 *
662 * @param string $request request.
663 *
664 * @return array|WP_Error
665 */
666 public function get_step_general_info($request)
667 {
668 $step_type = '';
669 $step_id = isset($request['step_id']) ? $request['step_id'] : null;
670 $step_type = get_post_meta($step_id, '_step_type', true);
671 $custom_script = get_post_meta($step_id, '_wpfnl_custom_script', true);
672 $custom_css = get_post_meta($step_id, '_wpfnl_custom_css', true);
673
674 $response = array(
675 'step_type' => $step_type,
676 'step_title' => get_the_title($step_id),
677 'step_view_link' => get_post_permalink($step_id),
678 'custom_script' => html_entity_decode($custom_script),
679 'custom_css' => html_entity_decode($custom_css)
680 );
681 return $this->prepare_item_for_response($response, $request);
682 }
683
684
685 /**
686 * Get the funnel title and link
687 *
688 * @param $request
689 *
690 * @return \WP_REST_Response
691 */
692 public function get_funnel_info($request)
693 {
694 $funnel_id = $request['funnel_id'];
695 $title = html_entity_decode(get_the_title($funnel_id));
696 $steps = get_post_meta($funnel_id, '_steps_order', true);
697 $response['success'] = false;
698 if ($steps) {
699 if (isset($steps[0]) && $steps[0]['id']) {
700 $response['link'] = get_post_permalink($steps[0]['id']);
701 $response['title'] = $title;
702 $utm_params = $this->get_utm_params($funnel_id);
703 if ($utm_params != '') {
704 $response['link'] = $response['link'] . $utm_params;
705 $response['title'] = $title;
706 }
707 $response['success'] = true;
708 }
709 }
710 return $this->prepare_item_for_response($response, $request);
711 }
712
713 /**
714 * Retrieves the funnel data based on the provided request.
715 *
716 * @param mixed $request The request data.
717 * @return WP_Rest_Response The prepared funnel data response.
718 *
719 * @since 2.7.17
720 */
721 public function get_funnel_data($request)
722 {
723 $funnel_id = isset($request['funnel_id']) ? $request['funnel_id'] : null;
724 $response = [
725 'status' => 'error',
726 'success' => false
727 ];
728
729 if (!$funnel_id) {
730 return $this->prepare_item_for_response($response, $request);
731 }
732
733 if( version_compare(WPFNL_VERSION, '3.0.0', '>=') && 'yes' !== get_post_meta( $funnel_id, 'wpfnls_is_newui_migrated', true ) ){
734 $migration = new Migration();
735 $funnel_data = get_post_meta($funnel_id, 'funnel_data', true) ? get_post_meta($funnel_id, 'funnel_data', true) : get_post_meta($funnel_id, '_funnel_data', true);
736 $migration->update_funnel_data($funnel_id, $funnel_data);
737 update_post_meta($funnel_id, 'wpfnls_is_newui_migrated', 'yes');
738 }
739 $response = $this->prepare_funnel_data_response($funnel_id);
740 return $this->prepare_item_for_response($response, $request);
741 }
742
743
744 /**
745 * Prepares the response data for the funnel data based on the provided parameters.
746 *
747 * @param int $funnel_id The ID of the funnel.
748 * @param mixed $request The request data.
749 * @return array The prepared response data.
750 *
751 * @since 2.7.17
752 */
753
754 /**
755 * Restore node_identifier precision from the html field.
756 *
757 * PHP json_encode truncates floats, so node_identifier stored as a number loses digits.
758 * The html field is a string (e.g. "addstep311.7949003047232") and preserves full precision.
759 * This method extracts node_identifier from html so the API response always matches what JS stored.
760 *
761 * @param array $funnel_data
762 * @return array
763 */
764 private function fix_node_identifier_precision($funnel_data)
765 {
766 if (!isset($funnel_data['drawflow']['Home']['data']) || !is_array($funnel_data['drawflow']['Home']['data'])) {
767 return $funnel_data;
768 }
769 foreach ($funnel_data['drawflow']['Home']['data'] as &$node) {
770 if (!isset($node['html'], $node['data']['node_identifier'])) {
771 continue;
772 }
773 $html = $node['html'];
774 if (strpos($html, 'addstep') === 0) {
775 $node['data']['node_identifier'] = substr($html, 7);
776 } elseif (strpos($html, 'conditional') === 0) {
777 $node['data']['node_identifier'] = substr($html, 11);
778 }
779 }
780 unset($node);
781 return $funnel_data;
782 }
783
784 private function prepare_funnel_data_response($funnel_id)
785 {
786 $response = [
787 'status' => 'error',
788 'success' => false
789 ];
790
791 $is_new_ui_data = get_post_meta($funnel_id, 'wpfnls_is_newui_migrated', true);
792 $funnel_data = 'yes' === $is_new_ui_data ? get_post_meta($funnel_id, '_funnel_data', true) : get_post_meta($funnel_id, 'funnel_data', true);
793 $funnel_identifier = get_post_meta($funnel_id, 'funnel_identifier', true);
794 $_steps_order = get_post_meta($funnel_id, '_steps', true);
795 $_steps_order = is_array($_steps_order) ? $_steps_order : [];
796 $status = get_post_status($funnel_id);
797 $title = html_entity_decode(get_the_title($funnel_id));
798 $first_step_id = Wpfnl_functions::get_first_step($funnel_id);
799
800 // Notice: We recently updated the architecture of our funnel template showcase site to version 3.1.3.
801 // If user's site's template data isn't synchronized with the latest data from our showcase site,
802 // user may encounter issues importing templates. Unfortunately, this situation is unavoidable.
803 // To ensure a smooth experience, we'll display a notice if user's site's template data needs updating.
804 $is_imported_funnel = get_post_meta($funnel_id, '_is_imported', true);
805 $show_template_sync_notice = false;
806 if ('yes' === $is_new_ui_data && 'yes' === $is_imported_funnel && !$funnel_data) {
807 $show_template_sync_notice = true;
808 }
809
810 /**
811 * Fires to modify funnel view link for A/B testing
812 *
813 * @param string $link
814 * @param int $step_id
815 * @param int $funnel_id
816 *
817 * @return string $link
818 * @since 1.7.8
819 */
820 $link = apply_filters('wpfunnels/modify_funnel_view_link', get_post_permalink($first_step_id), $first_step_id, $funnel_id);
821 $response['title'] = $title;
822
823 $utm_settings = Wpfnl_functions::get_funnel_utm_settings($funnel_id);
824 $view_link = !empty($response['link']) ? $response['link'] : $link;
825
826 if ($utm_settings['utm_enable'] == 'on') {
827 unset($utm_settings['utm_enable']);
828 $view_link = add_query_arg($utm_settings, $view_link);
829 $view_link = strtolower($view_link);
830 $utm_settings['utm_enable'] = 'on';
831 }
832
833 $response['success'] = true;
834 $step_order_data = self::get_steps_order_data($_steps_order);
835 $response['steps_order'] = isset( $step_order_data['steps_order'] ) ? $step_order_data['steps_order'] : [];
836 $response['is_ob'] = isset( $step_order_data['is_ob'] ) ? $step_order_data['is_ob'] : false;
837 $response['ob_steps'] = isset( $step_order_data['ob_steps'] ) ? $step_order_data['ob_steps'] : [];
838
839 $init_position = [
840 'pos_x' => 383,
841 'pos_y' => 143
842 ];
843
844 $funnel_data = Wpfnl_functions::remove_disconnected_addstep_node($funnel_data, $funnel_id);
845 $funnel_data = $this->fix_node_identifier_precision($funnel_data);
846
847 if ($funnel_data) {
848 $response = [
849 'status' => 'success',
850 'funnel_data' => $funnel_data,
851 'funnel_identifier' => $funnel_identifier,
852 'steps_order' => isset ($response['steps_order']) ? $response['steps_order'] : [],
853 'funnel_status' => $status,
854 'title' => $title,
855 'link' => $view_link,
856 'reset_funnel' => get_post_meta($funnel_id, '_wpfnl_is_reset', true),
857 'is_ob' => isset ($response['is_ob']) ? $response['is_ob'] : false,
858 'ob_steps' => isset ($response['ob_steps']) ? $response['ob_steps'] : []
859 ];
860
861 if (empty($funnel_data['drawflow']['Home']['data'])) {
862 $data = $this->get_default_add_step_node_data($init_position);
863 $response['status'] = 'scratch-funnel';
864 $response['funnel_data'] = $data['data'];
865 $response['node_identifier'] = $data['node_identifier'];
866 }
867
868 $response['funnel_data'] = $this->update_step_id_in_funnel_data_and_identifier($response['funnel_data'], $utm_settings);
869
870 /**
871 * Fires to update funnel data response in case of A/B Testing
872 *
873 * @param array $response
874 *
875 * @return array $response
876 **@since 1.7.8
877 */
878 $response = apply_filters('wpfunnels/update_funnel_data_response', $response);
879 if (!empty($response['ab_data'])) {
880 $response['ab_data'] = $this->update_step_view_link_with_utm_params($response['ab_data'], $utm_settings);
881 }
882 } elseif ($title && $status) {
883
884 $data = $this->get_default_add_step_node_data($init_position);
885 $response = array(
886 'status' => 'scratch-funnel',
887 'title' => $title,
888 'funnel_status' => $status,
889 'funnel_data' => $data['data'],
890 'reset_funnel' => get_post_meta($funnel_id, '_wpfnl_is_reset', true),
891 'node_identifier' => $data['node_identifier'],
892 'show_template_sync_notice' => $show_template_sync_notice,
893 );
894 }
895
896
897 $response['first_node_id'] = $this->get_first_step_node($first_step_id, $response['funnel_data']);
898 $response['conditional_data'] = $this->get_conditional_step($response['funnel_data']);
899 $response['funnel_analytics'] = self::get_funnel_analytics($funnel_id);
900 return $response;
901 }
902
903
904 /**
905 * Update funnel identifier
906 *
907 * @param $remote_step
908 * @param $new_step
909 * @param $args
910 *
911 * @return void
912 */
913 public function update_step_id_in_funnel_data_and_identifier($funnel_flow_array, $utm_settings)
914 {
915
916 if (!isset($funnel_flow_array['drawflow']['Home']['data']) || !is_array($funnel_flow_array['drawflow']['Home']['data'])) {
917 return $funnel_flow_array;
918 }
919 $isUtm = false;
920 if (isset($utm_settings['utm_enable']) && $utm_settings['utm_enable'] == 'on') {
921 $isUtm = true;
922 unset($utm_settings['utm_enable']);
923 }
924
925 // Loop through each step in funnel
926 foreach ($funnel_flow_array['drawflow']['Home']['data'] as &$step) {
927
928 // Find corresponding step in step id data
929 if ('conditional' !== $step['data']['step_type'] && 'addstep' !== $step['data']['step_type'] ) {
930
931 $step_view_link = get_post_permalink($step['data']['step_id']);
932 $step_view_link = $isUtm ? add_query_arg($utm_settings, $step_view_link) : $step_view_link;
933 // Check if step id is not in used_step_types array
934 $post_edit_link = base64_encode(get_edit_post_link($step['data']['step_id']));
935 $post_view_link = base64_encode($step_view_link);
936
937 $step['data']['step_edit_link'] = $post_edit_link;
938 $step['data']['step_view_link'] = $post_view_link;
939 }
940 }
941 return $funnel_flow_array;
942 }
943
944
945 /**
946 * get conditional steps with validity checking
947 *
948 * @param $funnel_flow_array
949 *
950 * @return array
951 *
952 * @since 3.4.13
953 */
954 public function get_conditional_step($funnel_flow_array){
955 if (!isset($funnel_flow_array['drawflow']['Home']['data']) || !is_array($funnel_flow_array['drawflow']['Home']['data'])) {
956 return [];
957 }
958
959 $condition_data = [];
960 foreach ($funnel_flow_array['drawflow']['Home']['data'] as &$step) {
961
962 // Find corresponding step in step id data
963 if ('conditional' !== $step['data']['step_type'] && 'addstep' !== $step['data']['step_type'] ) {
964 $step_id = $step['data']['step_id'];
965 $is_condition_enabled = get_post_meta($step_id, '_wpfnl_maybe_enable_condition', true);
966 if( 'yes' === $is_condition_enabled ){
967 $data = [
968 'step_id' => $step_id,
969 'data' => 'no'
970 ];
971 $conditions = get_post_meta($step_id, '_wpfnl_step_conditions', true);
972 if( is_array($conditions) && !empty($conditions) ){
973 $data['data'] = 'yes';
974 }
975 array_push($condition_data, $data);
976 }
977 }
978 }
979 return $condition_data;
980 }
981
982 /**
983 * Updates A/B test variations' step view links with UTM parameters.
984 *
985 * This method takes A/B test data, specifically variations, and appends UTM parameters
986 * to the step view links of each variation. It does so based on the provided UTM settings
987 * if UTM tracking is enabled.
988 *
989 * @param array $ab_data An array containing A/B test data with variations.
990 * @param array $utm_settings An array of UTM parameters and their values.
991 *
992 * @return array The modified A/B test data with UTM parameters appended to step view links.
993 * @since 2.8.14
994 */
995 private function update_step_view_link_with_utm_params($ab_data, $utm_settings)
996 {
997 if (!empty($ab_data) && !empty($utm_settings['utm_enable']) && 'on' === $utm_settings['utm_enable']) {
998 unset($utm_settings['utm_enable']);
999 foreach ($ab_data as $ab_key => $data) {
1000 $variations = $data['data']['start_settings']['variations'] ?? [];
1001 if (!empty($variations)) {
1002 foreach ($variations as $var_key => $variation) {
1003 if (!empty($variation['step_view_link'])) {
1004 $ab_data[$ab_key]['data']['start_settings']['variations'][$var_key]['step_view_link'] = add_query_arg($utm_settings, $variation['step_view_link']);
1005 }
1006 }
1007 }
1008 }
1009 }
1010 return $ab_data;
1011 }
1012
1013 /**
1014 * Get the first step Node
1015 */
1016 public function get_first_step_node($first_step_id, $funnel_data)
1017 {
1018 $data = isset($funnel_data['drawflow']['Home']['data']) ? $funnel_data['drawflow']['Home']['data'] : [];
1019 foreach ($data as $step_data) {
1020 if (isset($step_data['data']['step_id']) && $step_data['data']['step_id'] == $first_step_id) {
1021 return $step_data['id'];
1022 }
1023 }
1024 }
1025
1026
1027 /**
1028 * Get default add step node data and identifire for canvas
1029 *
1030 * @return Array Returns an array containing the default add step node data and identifier
1031 * @since 2.8.0
1032 *
1033 */
1034 public function get_default_add_step_node_data($init_position)
1035 {
1036 $node_identifier = rand() * (500 - 100) + 100;
1037 $data = [
1038 'drawflow' => [
1039 'Home' => [
1040 'data' => [
1041 1 => [
1042 'id' => 1,
1043 'name' => 'addstep',
1044 'data' => [
1045 'step_type' => 'addstep',
1046 'node_identifier' => $node_identifier
1047 ],
1048 'class' => 'addstep',
1049 'html' => 'addstep' . $node_identifier,
1050 'typenode' => 'vue',
1051 'inputs' => [
1052 'input_1' => [
1053 'connections' => []
1054 ]
1055 ],
1056 'outputs' => [],
1057 'pos_x' => isset($init_position['pos_x']) ? $init_position['pos_x'] : 383,
1058 'pos_y' => isset($init_position['pos_y']) ? $init_position['pos_y'] : 143,
1059 ]
1060 ]
1061 ]
1062 ]
1063 ];
1064 return [
1065 'data' => $data,
1066 'node_identifier' => $node_identifier,
1067 ];
1068 }
1069
1070 /**
1071 * Get formatted steps order data.
1072 *
1073 * Formats the steps order data by populating additional fields and determining if any step is an order bump.
1074 *
1075 * @param array $steps_order The array of funnel steps order.
1076 * @return array Formatted steps order data with additional fields and order bump flag.
1077 *
1078 * @since 2.7.17
1079 */
1080 private static function get_steps_order_data($steps_order)
1081 {
1082 $is_order_bump = false;
1083 $ob_steps = [];
1084 $formatted_steps_order = [];
1085 foreach ($steps_order as $step) {
1086 $step_id = isset($step['id']) ? $step['id'] : null;
1087 $_temp_step = $step;
1088 $analytics = self::get_step_analytics($step_id);
1089 $_temp_step['visit'] = $analytics['visit'];
1090 $_temp_step['conversion'] = $analytics['conversion'];
1091 $_temp_step['name'] = get_the_title($step_id);
1092 $_step_type = get_post_meta($step_id, '_step_type', true);
1093 $should_assign_product = in_array($_step_type, ['checkout', 'upsell', 'downsell']) && !get_post_meta($step_id, '_wpfnl_' . $_step_type . '_products', true);
1094
1095 if ('checkout' === $_step_type) {
1096 $step_has_ob = self::is_order_bump($step_id);
1097 if ($step_has_ob) {
1098 $is_order_bump = true;
1099 $ob_steps[] = (int) $step_id;
1100 }
1101 }
1102
1103 $_temp_step['should_assign_product'] = $should_assign_product;
1104
1105 /**
1106 * Fires to add Mail Mint automation data
1107 *
1108 * @param array $step
1109 * @param int $step_id
1110 * @since 2.7.0
1111 */
1112 $formatted_steps_order[] = apply_filters('wpfunnels/step_data', $_temp_step, $step_id);
1113 }
1114
1115 return [
1116 'steps_order' => $formatted_steps_order,
1117 'is_ob' => $is_order_bump,
1118 'ob_steps' => $ob_steps,
1119 ];
1120 }
1121
1122 /**
1123 * Get visit and conversion analytics data for a step.
1124 *
1125 * Queries the analytics tables to get total visits and conversions for a given step.
1126 *
1127 * @param int $step_id The step ID.
1128 * @return array Array with 'visit' and 'conversion' counts.
1129 *
1130 * @since 3.5.0
1131 */
1132 private static function get_step_analytics($step_id)
1133 {
1134 $total = array(
1135 'visit' => 0,
1136 'conversion' => 0,
1137 );
1138
1139 if (!$step_id) {
1140 return $total;
1141 }
1142
1143 global $wpdb;
1144 $analytics_table = $wpdb->prefix . 'wpfnl_analytics';
1145 $analytics_meta_table = $wpdb->prefix . 'wpfnl_analytics_meta';
1146
1147 // Check if the analytics table exists before querying.
1148 $table_exists = $wpdb->get_var($wpdb->prepare("SHOW TABLES LIKE %s", $analytics_table));
1149 if (!$table_exists) {
1150 return $total;
1151 }
1152
1153 // Get total visits.
1154 $cache_key_visits = 'wpfnl_pro_visits_' . $step_id;
1155 $visits_data = wp_cache_get($cache_key_visits, 'wpfnl_pro');
1156
1157 if (false === $visits_data) {
1158 $query = $wpdb->prepare(
1159 "SELECT wpft1.step_id, COUNT( DISTINCT( wpft1.id ) ) AS total_visits
1160 FROM {$analytics_table} as wpft1
1161 WHERE wpft1.step_id = %s
1162 ORDER BY NULL",
1163 $step_id
1164 ); // phpcs:ignore
1165
1166 $visits_data = $wpdb->get_row($query); // phpcs:ignore
1167 wp_cache_set($cache_key_visits, $visits_data, 'wpfnl_pro', 3600);
1168 }
1169
1170 // Get conversions.
1171 $cache_key_conversion = 'wpfnl_pro_conversion_' . $step_id;
1172 $conversion_data = wp_cache_get($cache_key_conversion, 'wpfnl_pro');
1173
1174 if (false === $conversion_data) {
1175 $query = $wpdb->prepare(
1176 "SELECT wpft1.step_id, COUNT( CASE WHEN wpft2.meta_key = 'conversion' AND wpft2.meta_value = 'yes' THEN wpft1.step_id ELSE NULL END ) AS conversions
1177 FROM {$analytics_table} as wpft1
1178 INNER JOIN {$analytics_meta_table} as wpft2 ON wpft1.id = wpft2.analytics_id
1179 WHERE wpft1.step_id = %s
1180 ORDER BY NULL",
1181 $step_id
1182 ); // phpcs:ignore
1183
1184 $conversion_data = $wpdb->get_row($query); // phpcs:ignore
1185 wp_cache_set($cache_key_conversion, $conversion_data, 'wpfnl_pro', 3600);
1186 }
1187
1188 if ($visits_data) {
1189 $total['visit'] = $visits_data->total_visits;
1190 }
1191
1192 if ($conversion_data) {
1193 $total['conversion'] = $conversion_data->conversions;
1194 }
1195
1196 return $total;
1197 }
1198
1199 /**
1200 * Get funnel-level analytics data (total orders, revenue, AOV, orderbump revenue).
1201 *
1202 * Queries the wpfnl_stats table for completed orders belonging to a specific funnel.
1203 *
1204 * @param int $funnel_id The funnel ID.
1205 * @return array Analytics data with total_orders, total_revenue, aov, and orderbump_revenue.
1206 *
1207 * @since 3.5.0
1208 */
1209 private static function get_funnel_analytics($funnel_id)
1210 {
1211 $analytics = array(
1212 'total_orders' => 0,
1213 'total_revenue' => 0,
1214 'aov' => 0,
1215 );
1216
1217 if (!$funnel_id) {
1218 return $analytics;
1219 }
1220
1221 global $wpdb;
1222 $stats_table = $wpdb->prefix . 'wpfnl_stats';
1223
1224 // Check if the stats table exists before querying.
1225 $table_exists = $wpdb->get_var($wpdb->prepare("SHOW TABLES LIKE %s", $stats_table));
1226 if (!$table_exists) {
1227 return $analytics;
1228 }
1229
1230 $cache_key = 'wpfnl_funnel_analytics_' . $funnel_id;
1231 $cached_data = wp_cache_get($cache_key, 'wpfnl_pro');
1232
1233 if (false !== $cached_data) {
1234 return $cached_data;
1235 }
1236
1237 $row = $wpdb->get_row($wpdb->prepare(
1238 "SELECT
1239 COUNT(DISTINCT id) AS total_orders,
1240 COALESCE(SUM(total_sales), 0) AS total_revenue
1241 FROM {$stats_table}
1242 WHERE funnel_id = %d AND status IN (%s, %s)",
1243 $funnel_id,
1244 'completed',
1245 'processing'
1246 )); // phpcs:ignore
1247
1248 if ($row) {
1249 $analytics['total_orders'] = (int) $row->total_orders;
1250 $analytics['total_revenue'] = round((float) $row->total_revenue, 2);
1251 $analytics['aov'] = $analytics['total_orders'] > 0
1252 ? round($analytics['total_revenue'] / $analytics['total_orders'], 2)
1253 : 0;
1254 }
1255
1256 wp_cache_set($cache_key, $analytics, 'wpfnl_pro', 3600);
1257
1258 return $analytics;
1259 }
1260
1261 /**
1262 * Checks if a step in the funnel is an order bump.
1263 * Retrieves the order bump settings for the specified step and determines if it is an order bump.
1264 * @param array $steps_order The array of funnel steps.
1265 * @param int $step_id The ID of the step to check.
1266 *
1267 *
1268 * @return bool True if the step is an order bump, false otherwise.
1269 * @since 2.7.17
1270 *
1271 */
1272 public static function is_order_bump($step_id)
1273 {
1274 $all_settings = get_post_meta($step_id, 'order-bump-settings', true) ?: [];
1275 $is_multiple = Wpfnl_functions::check_array_is_multidimentional($all_settings);
1276
1277 if (!$is_multiple && $all_settings) {
1278 $all_settings['name'] = 'Order bump';
1279 $all_settings = Wpfnl_functions::migrate_order_bump($all_settings, $step_id);
1280 }
1281
1282 if (is_array($all_settings) && count($all_settings) > 0) {
1283 $funnel_id = get_post_meta($step_id, '_funnel_id', true);
1284 $type = get_post_meta($funnel_id, '_wpfnl_funnel_type', true) ?: 'wc';
1285 $class_object = Wpfnl_Controller_Type_Factory::build($type);
1286 if ($class_object) {
1287 $all_settings = $class_object->get_ob_settings($all_settings);
1288 }
1289 }
1290
1291
1292 if (count($all_settings)) {
1293 return true;
1294 }
1295 return false;
1296 }
1297
1298 /**
1299 * Get formatted funnel data
1300 *
1301 * @param $drawflow
1302 *
1303 * @return mixed
1304 *
1305 * @since 2.0.5
1306 */
1307 private function get_formatted_funnel_data($drawflow)
1308 {
1309 if (isset($drawflow['drawflow']['Home']['data'])) {
1310 $drawflow_data = $drawflow['drawflow']['Home']['data'];
1311 foreach ($drawflow_data as $key => $data) {
1312 $step_data = $data['data'];
1313 $step_type = $step_data['step_type'];
1314 if ('conditional' !== $step_type) {
1315 $step_id = $step_data['step_id'];
1316 $funnel_id = Wpfnl_functions::get_funnel_id_from_step($step_id);
1317 if ('conditional' !== $step_type) {
1318 $edit_post_link = get_edit_post_link($step_id);
1319 $view_link = get_the_permalink($step_id);
1320 $utm_params = $this->get_utm_params($funnel_id);
1321 if ($utm_params != '') {
1322 $view_link = $view_link . $utm_params;
1323 }
1324 $title = get_the_title($step_id);
1325 $drawflow['drawflow']['Home']['data'][$key]['data'] = array(
1326 'step_edit_link' => base64_encode($edit_post_link),
1327 'step_id' => $step_id,
1328 'step_type' => $step_data['step_type'],
1329 'step_view_link' => base64_encode(rtrim($view_link, '/')),
1330 'step_name' => $title,
1331 );
1332 }
1333 }
1334 }
1335 }
1336 return $drawflow;
1337 }
1338
1339 /**
1340 * Save funnel data.
1341 *
1342 * @param string $request request.
1343 *
1344 * @return array|WP_Error
1345 */
1346 public function save_funnel_data($request)
1347 {
1348 $funnel_id = $request['funnel_id'] ?? null;
1349 $funnel_json = $request['funnel_data'] ?? [];
1350 $funnel_identifier = $request['funnel_identifier'] ?? [];
1351 $should_update_steps_order = $request['should_update_steps_order'] ?? false;
1352 $should_update_steps = $request['should_update_steps'] ?? false;
1353 $funnel_data = [];
1354 $_steps = [];
1355 $response = ['success' => true, 'link' => home_url()];
1356
1357 if ($funnel_json) {
1358 $funnel_data = $funnel_json;
1359 $steps = $funnel_data['drawflow']['Home']['data'];
1360
1361 if (is_array($steps)) {
1362 foreach ($steps as $key => $step) {
1363 if ($step) {
1364 $node_data = $step['data'];
1365 if (isset($node_data["step_name"])) unset($node_data["step_name"]);
1366
1367 // Preserve node_identifier at full precision by extracting it from the html
1368 // field (a string) rather than using the float value decoded by json_decode,
1369 // which PHP truncates before we can read it.
1370 if (isset($step['html'], $node_data['node_identifier'])) {
1371 $html = $step['html'];
1372 if (strpos($html, 'addstep') === 0) {
1373 $node_data['node_identifier'] = substr($html, 7);
1374 } elseif (strpos($html, 'conditional') === 0) {
1375 $node_data['node_identifier'] = substr($html, 11);
1376 }
1377 }
1378
1379 $step['data'] = $node_data;
1380 $_steps[$key] = $step;
1381
1382 }
1383 }
1384 }
1385
1386 }
1387 $funnel_data['drawflow']['Home']['data'] = apply_filters('wpfunnels/modify_funnel_data', $_steps);
1388 update_post_meta($funnel_id, '_funnel_data', $funnel_data);
1389 update_post_meta($funnel_id, 'funnel_identifier', $funnel_identifier);
1390
1391 if ($should_update_steps) {
1392 $steps = $this->get_steps($funnel_data);
1393 update_post_meta($funnel_id, '_steps', $steps);
1394 Wpfnl_functions::generate_first_step($funnel_id, $steps);
1395 }
1396
1397 $_steps_order = $this->get_steps_order($funnel_data);
1398 $key = array_search('checkout', array_column($_steps_order, 'step_type'));
1399 if (false !== $key) {
1400 $funnel_type = get_post_meta($funnel_id, '_wpfnl_funnel_type', true);
1401 if ('lead' === $funnel_type) {
1402 if (Wpfnl_functions::is_wc_active()) {
1403 update_post_meta($funnel_id, '_wpfnl_funnel_type', 'wc');
1404 } else {
1405 // Check if LMS add-on is active AND at least one LMS plugin (LearnDash or CreatorLMS) is active
1406 if (Wpfnl_functions::is_lms_addon_active() && Wpfnl_functions::is_any_lms_plugin_active()) {
1407 update_post_meta($funnel_id, '_wpfnl_funnel_type', 'lms');
1408 }
1409 }
1410 }
1411 }
1412 if ($should_update_steps_order) {
1413 $steps_order = array();
1414 foreach ($_steps_order as $_step) {
1415 if (count($_step)) {
1416 $steps_order[] = $_step;
1417 }
1418 }
1419
1420 if (count($steps_order)) {
1421 update_post_meta($funnel_id, '_steps_order', $steps_order);
1422
1423 } else {
1424 delete_post_meta($funnel_id, '_steps_order');
1425 }
1426 }
1427
1428 $response['step_id'] = Wpfnl_functions::get_first_step($funnel_id);
1429
1430 // Fallback for existing users' existing funnel
1431 // For new funnel, this condition should not trigger
1432 if (!$response['step_id']) {
1433 Wpfnl_functions::generate_first_step($funnel_id);
1434 $response['step_id'] = Wpfnl_functions::get_first_step($funnel_id);
1435 }
1436
1437 $utm_settings = Wpfnl_functions::get_funnel_utm_settings($funnel_id);
1438 $view_link = get_post_permalink($response['step_id']);
1439
1440 if (!empty($utm_settings['utm_enable']) && 'on' === $utm_settings['utm_enable']) {
1441 unset($utm_settings['utm_enable']);
1442 $view_link = add_query_arg($utm_settings, $view_link);
1443 $view_link = strtolower($view_link);
1444 }
1445
1446 $response['link'] = esc_url($view_link);
1447
1448 $response['funnel_id'] = $funnel_id;
1449 $response['success'] = true;
1450 $response['funnel_type'] = get_post_meta($funnel_id, '_wpfnl_funnel_type', true);
1451 $response = apply_filters('wpfunnels/update_funnel_link', $response);
1452 do_action('wpfunnels/after_save_funnel_data', $funnel_id);
1453 if (isset($request['mintSteps'])) {
1454 do_action('wpfunnels/save_mint_automation', $funnel_id, $request['mintSteps']);
1455 }
1456 Wpfnl_functions::generate_first_step($funnel_id);
1457
1458 do_action( 'wpfunnels_canvas_saved', absint( $funnel_id ) );
1459
1460 return rest_ensure_response($response);
1461 }
1462
1463
1464 /**
1465 * Get steps
1466 *
1467 * @param $funnel_flow_data
1468 *
1469 * @return array
1470 *
1471 * @since 2.0.5
1472 */
1473 private function get_steps($funnel_flow_data)
1474 {
1475 $drawflow = $funnel_flow_data['drawflow'];
1476 $steps = array();
1477 if (isset($drawflow['Home']['data'])) {
1478 $drawflow_data = $drawflow['Home']['data'];
1479 foreach ($drawflow_data as $key => $data) {
1480 $step_data = $data['data'];
1481 if ('conditional' !== $step_data['step_type'] && 'addstep' !== $step_data['step_type']) {
1482 $step_id = $step_data['step_id'];
1483 $step_type = $step_data['step_type'];
1484 $step_name = sanitize_text_field(get_the_title($step_data['step_id']));
1485 $steps[] = array(
1486 'id' => $step_id,
1487 'step_type' => $step_type,
1488 'name' => $step_name,
1489 );
1490 }
1491 }
1492 }
1493 return $steps;
1494 }
1495
1496
1497 /**
1498 * Get steps order
1499 *
1500 * @param $funnel_flow_data
1501 *
1502 * @return array
1503 *
1504 * @since 2.0.5
1505 */
1506 public function get_steps_order($funnel_flow_data)
1507 {
1508 $drawflow = $funnel_flow_data['drawflow'];
1509 $nodes = array();
1510 $step_order = array();
1511 $first_node_id = '';
1512 $start_node = array();
1513
1514
1515 if (isset($drawflow['Home']['data'])) {
1516 $drawflow_data = $drawflow['Home']['data'];
1517
1518 /**
1519 * If has only one step, that only step will be the first step, no conditions should be checked.
1520 * just return the step order
1521 */
1522 if (1 === count($drawflow_data)) {
1523 $node_id = array_keys($drawflow_data)[0];
1524 $data = $drawflow_data[$node_id];
1525 $step_data = isset($data['data']) ? $data['data'] : array();
1526 $step_type = isset($step_data['step_type']) ? $step_data['step_type'] : '';
1527 $step_id = isset($step_data['step_id']) ? $step_data['step_id'] : 0;
1528 $step_order[] = array(
1529 'id' => $step_id,
1530 'step_type' => $step_type,
1531 'name' => sanitize_text_field(get_the_title($step_id)),
1532 );
1533 return $step_order;
1534
1535 }
1536
1537 /**
1538 * First we will find the first node (the node which has only output connection but no input connection will be considered as first node) and the list of nodes array which has the
1539 * step information includes output connection and input connection and it will be stored on $nodes
1540 */
1541 foreach ($drawflow_data as $key => $data) {
1542 $step_data = $data['data'];
1543
1544 $step_type = $step_data['step_type'];
1545 $step_id = 'conditional' !== $step_type && 'addstep' !== $step_type ? $step_data['step_id'] : 0;
1546 if (
1547 (isset($data['outputs']['output_1']['connections']) && count($data['outputs']['output_1']['connections'])) ||
1548 (isset($data['inputs']['input_1']['connections']) && count($data['inputs']['input_1']['connections']))
1549 ) {
1550
1551
1552 if ('conditional' === $step_type || 'addstep' === $step_type) {
1553 continue;
1554 }
1555
1556 /**
1557 * A starting node is a node which has only output connection but not any input connection.
1558 * if the step is landing, then there should not be any input connection for this step. so we will only consider the output connection for landing only.
1559 * for other step types (checkout, offer, thankyou), we will check if the step has any output connection and no input connection.
1560 */
1561 if ('landing' === $step_type) {
1562 if (
1563 isset($data['outputs']['output_1']['connections']) && count($data['outputs']['output_1']['connections']) &&
1564 (isset($data['inputs']) && (count($data['inputs']) == 0 || (isset($data['inputs']['input_1']['connections']) && count($data['inputs']['input_1']['connections']) == 0)))
1565 ) {
1566 $start_node = array(
1567 'id' => $step_id,
1568 'step_type' => $step_type,
1569 'name' => sanitize_text_field(get_the_title($step_id)),
1570 );
1571 }
1572 } else {
1573 if (
1574 isset($data['outputs']['output_1']['connections']) && count($data['outputs']['output_1']['connections']) &&
1575 (isset($data['inputs']['input_1']['connections']) && count($data['inputs']['input_1']['connections']) === 0)
1576 ) {
1577 $start_node = array(
1578 'id' => $step_id,
1579 'step_type' => $step_type,
1580 'name' => sanitize_text_field(get_the_title($step_id)),
1581 );
1582 } else {
1583 $step_order[] = array(
1584 'id' => $step_id,
1585 'step_type' => $step_type,
1586 'name' => sanitize_text_field(get_the_title($step_id)),
1587 );
1588 }
1589 }
1590 }
1591 }
1592
1593 $step_order = $this->array_insert($step_order, $start_node, 0);
1594 }
1595 return $step_order;
1596 }
1597
1598
1599 /**
1600 * Array insert element on position
1601 *
1602 * @param $original
1603 * @param $inserted
1604 * @param int $position
1605 *
1606 * @return mixed
1607 */
1608 private function array_insert(&$original, $inserted, $position)
1609 {
1610 array_splice($original, $position, 0, array($inserted));
1611 return $original;
1612 }
1613
1614
1615 /**
1616 * Export funnel data
1617 *
1618 * @param $request
1619 *
1620 * @return false|string
1621 *
1622 * @since 1.0.0
1623 */
1624 public function export_funnel($request)
1625 {
1626 $funnel_id = $request['funnel_id'];
1627
1628 //===main array of data which will be downloaded as json file===//
1629 $data = array();
1630
1631 $funnel_title = get_the_title($funnel_id);
1632 $funnel_meta = get_post_meta($funnel_id);
1633
1634 //=== Added title and meta of current funnel post===//
1635 $data['title'] = $funnel_title;
1636 $data['meta'] = $funnel_meta;
1637
1638 //=== Find list of steps and ther data===//
1639 //== Getting steps from identifier meta==//
1640 if (isset($data['meta']['funnel_identifier'])) {
1641 $identifier_meta = $data['meta']['funnel_identifier'];
1642 $all_steps_array = array();
1643 foreach ($identifier_meta as $identifier_meta_key => $identifier_meta_value) {
1644 $node_step_pair = json_decode($identifier_meta_value, true);
1645 foreach ($node_step_pair as $node_step_pair_key => $node_step_pair_value) {
1646 $current_steps_array = array();
1647 $step_id = $node_step_pair_value;
1648 $step_title = get_the_title($step_id);
1649 $step_meta = get_post_meta($step_id);
1650 $content_post = get_post($step_id);
1651 $content = json_encode($content_post->post_content);
1652 $current_steps_array['step_id'] = $step_id;
1653 $current_steps_array['title'] = $step_title;
1654 $current_steps_array['meta'] = $step_meta;
1655 $current_steps_array['content'] = $content;
1656 $all_steps_array[] = $current_steps_array;
1657 }
1658 }
1659 $data['steps'] = $all_steps_array;
1660 }
1661
1662 return json_encode($data);
1663 }
1664
1665 /**
1666 * Get UTM Params URL
1667 */
1668
1669 public function get_utm_params($funnel_id)
1670 {
1671 $utm_params = '';
1672 $utm_settings = $this->get_utm_settings($funnel_id);
1673 $utm_params = '?utm_source=' . 'fgddffd' . '&utm_medium=' . 'ghfds' . '&utm_campaign=' . 'dsaghfds';
1674 return $utm_params;
1675 if ($utm_settings['utm_enable'] == 'on') {
1676 $utm_params = '?utm_source=' . $utm_settings['utm_source'] . '&utm_medium=' . $utm_settings['utm_medium'] . '&utm_campaign=' . $utm_settings['utm_campaign'];
1677 $utm_params .= ((!empty($utm_settings['utm_content'])) ? '&utm_content=' . $utm_settings['utm_content'] : '');
1678 $utm_params = strtolower($utm_params);
1679 }
1680 return $utm_params;
1681 }
1682
1683 /**
1684 * Get GTM Settings
1685 *
1686 * @return array
1687 */
1688 public function get_utm_settings($funnel_id)
1689 {
1690 $default_settings = array(
1691 'utm_enable' => 'off',
1692 'utm_source' => '',
1693 'utm_medium' => '',
1694 'utm_campaign' => '',
1695 'utm_content' => '',
1696 );
1697 $utm_settings = get_post_meta($funnel_id, '_wpfunnels_utm_params', true);
1698 return wp_parse_args($utm_settings, $default_settings);
1699 }
1700
1701
1702 /**
1703 * Prepare a single setting object for response.
1704 *
1705 * @param object|array $item Setting object.
1706 * @param WP_REST_Request $request Request object.
1707 *
1708 * @return \WP_REST_Response $response Response data.
1709 * @since 1.0.0
1710 */
1711 public function prepare_item_for_response($item, $request)
1712 {
1713 $data = $this->add_additional_fields_to_object($item, $request);
1714 return rest_ensure_response($data);
1715 }
1716
1717
1718 /**
1719 * Get GBF data
1720 *
1721 * @param $request
1722 *
1723 * @return WP_Error|\WP_REST_Response
1724 */
1725 public function get_GBF_data($request)
1726 {
1727
1728 $funnel_id = $request['funnel_id'];
1729 $steps = Wpfnl_functions::get_steps($funnel_id);
1730
1731 if ( ! Wpfnl_functions::is_global_funnel_activated() ) {
1732 $response = array(
1733 'success' => false,
1734 'data' => 'Global Funnel is not activated'
1735 );
1736 return rest_ensure_response($response);
1737 }
1738
1739 $is_gbf = get_post_meta($funnel_id, 'is_global_funnel', true);
1740 if ('yes' === $is_gbf) {
1741 $start_condition = get_post_meta($funnel_id, 'global_funnel_start_condition', true);
1742 $step_ids = array();
1743 foreach ($steps as $step) {
1744 if ($step['step_type'] == 'checkout') {
1745 if (!empty($start_condition)) {
1746 array_push($step_ids, $step['id']);
1747 }
1748 } elseif ($step['step_type'] == 'upsell') {
1749 $upsell_rules = get_post_meta($step['id'], 'global_funnel_upsell_rules', true);
1750 if (!empty($upsell_rules)) {
1751 array_push($step_ids, $step['id']);
1752 }
1753 } elseif ($step['step_type'] == 'downsell') {
1754 $downsell_rules = get_post_meta($step['id'], 'global_funnel_downsell_rules', true);
1755 if (!empty($downsell_rules)) {
1756 array_push($step_ids, $step['id']);
1757 }
1758 }
1759 }
1760 }
1761 $response = array(
1762 'success' => false,
1763 'data' => []
1764 );
1765 return rest_ensure_response($response);
1766 }
1767
1768
1769 /**
1770 * Get ab testing default setttings
1771 *
1772 * @param WP_REST_Request $request
1773 *
1774 * @return Array
1775 *
1776 * @since 1.6.21
1777 */
1778 public function get_ab_settings(WP_REST_Request $request)
1779 {
1780
1781 $response = [];
1782 if (isset($request['step_id'])) {
1783 $step_id = $request['step_id'];
1784 $default_settings = $this->get_default_start_setting($step_id);
1785 $response['data'] = $default_settings;
1786 $response['success'] = true;
1787 } else {
1788 $response['data'] = '';
1789 $response['success'] = false;
1790 }
1791
1792 return rest_ensure_response($response);
1793 }
1794
1795 /**
1796 * Change funnel name
1797 *
1798 * @param WP_REST_Request $payload Funnel name payload.
1799 *
1800 * @return array
1801 * @since 2.7.5
1802 */
1803 public function funnel_name_change($payload)
1804 {
1805 if (!isset($payload['funnel_id'], $payload['funnel_name'])) {
1806 return new WP_Error(
1807 'rest_invalid_request_params',
1808 __('Invalid request params.'),
1809 array('status' => 404)
1810 );
1811 }
1812
1813 $funnel_id = sanitize_text_field($payload['funnel_id']);
1814 $updated_name = sanitize_text_field($payload['funnel_name']);
1815 Wpfnl::$instance->funnel_store->set_id($funnel_id);
1816 Wpfnl::$instance->funnel_store->update_funnel_name($updated_name);
1817 self::update_steps_url_on_funnel_name_update($funnel_id);
1818 flush_rewrite_rules();
1819
1820 $response = array(
1821 'success' => true,
1822 'message' => 'Funnel name changed successfully',
1823 'funnelID' => $funnel_id,
1824 'name' => $updated_name
1825 );
1826
1827 return rest_ensure_response($response);
1828 }
1829
1830 /**
1831 * Update the URL and funnel name for each step in a funnel.
1832 *
1833 * This function retrieves the step IDs associated with a specific funnel,
1834 * and updates the URL and funnel name for each step based on the step ID.
1835 *
1836 * @param int $funnel_id The ID of the funnel.
1837 * @return void
1838 * @since 2.7.10
1839 */
1840 public function update_steps_url_on_funnel_name_update($funnel_id)
1841 {
1842 $step_controller = new StepController();
1843 $step_ids = Wpfnl_functions::get_step_ids($funnel_id);
1844 foreach ($step_ids as $step_id) {
1845 $step_title = get_post_meta($step_id, '_wpf_step_title', true);
1846 $step_slug = get_post_meta($step_id, '_wpf_step_slug', true);
1847 $settings = array(
1848 'funnel_id' => $funnel_id,
1849 'step_id' => $step_id,
1850 'title' => !empty($step_title) ? $step_title : get_the_title($step_id),
1851 'slug' => !empty($step_slug) ? $step_slug : get_post_field('post_name', $step_id)
1852 );
1853 $step_controller->update_step_meta_on_funnel_name_change($funnel_id, $step_id, $settings);
1854 }
1855 }
1856
1857 /**
1858 * Get funnel settings by funnel id
1859 *
1860 * @param array $payload Funnel Settings payload data.
1861 * @return WP_Rest_Response
1862 *
1863 * @since 1.0.0
1864 */
1865 public function wpfnl_get_funnel_settings($payload)
1866 {
1867 // Early return in case of missing funnel id.
1868 if (!isset($payload['funnel_id'])) {
1869 return $this->prepare_wp_error_response(
1870 'rest_missing_funnelID',
1871 __('Funnel id is missing', 'wpfnl'),
1872 array(
1873 'status' => 400
1874 )
1875 );
1876 }
1877
1878 // To get global settings.
1879 $global_gtm = Wpfnl_functions::get_gtm_settings();
1880 $global_pixel = Wpfnl_functions::get_facebook_pixel_settings();
1881 $settings = Wpfnl_functions::get_funnel_settings($payload['funnel_id']);
1882
1883 // Set default skip offer settings.
1884 $skip_settings = array(
1885 'skip_offer' => 'no',
1886 'skip_if_quantity' => 'no',
1887 );
1888
1889 if (get_post_meta($payload['funnel_id'], '_wpfunnels_skip_offer', true)) {
1890 $skip_settings = get_post_meta($payload['funnel_id'], '_wpfunnels_skip_offer', true);
1891 }
1892
1893 // Preparing rest response.
1894 $response = array(
1895 'success' => true,
1896 'data' => $settings,
1897 'globalGtm' => isset ($global_gtm['gtm_enable']) ? $global_gtm['gtm_enable'] : 'no',
1898 'globalPixel' => isset ($global_pixel['enable_fb_pixel']) ? $global_pixel['enable_fb_pixel'] : 'no',
1899 'skipSettings' => $skip_settings,
1900 'skipRecurringOffer' => get_post_meta($payload['funnel_id'], '_wpfunnels_skip_recurring_offer', true),
1901 'skipRecurringOfferWithinDays' => sanitize_text_field(get_post_meta($payload['funnel_id'], '_wpfunnels_skip_recurring_offer_within_days', true)),
1902 );
1903
1904 /**
1905 * Fires to get an individual funnel settings.
1906 *
1907 * @param string $response The settings data.
1908 * @param string $payload ['funnel_id'] The individual funnel id.
1909 * @since 1.0.0
1910 *
1911 */
1912 $response = apply_filters('wpfunnels/funnel_individual_settings', $response, $payload['funnel_id']);
1913 $response = rest_ensure_response($response);
1914
1915 return $response;//phpcs:ignore
1916 }
1917
1918 /**
1919 * Delete marked funnel
1920 *
1921 * @param $payload
1922 *
1923 * @return array|bool
1924 * @since 1.0.0
1925 */
1926 public function delete_marked_funnels($payload)
1927 {
1928 $funnel_controller = Module::instance();
1929
1930 return $funnel_controller->delete_marked_funnels($payload);
1931 }
1932
1933
1934 /**
1935 * Restore marked funnel
1936 *
1937 * @param $payload
1938 *
1939 * @return array|bool
1940 * @since 3.1.8
1941 */
1942 public function restore_marked_funnels($payload)
1943 {
1944 $funnel_controller = Module::instance();
1945
1946 return $funnel_controller->restore_marked_funnels($payload);
1947 }
1948
1949 /**
1950 * Restore marked funnel
1951 *
1952 * @param $payload
1953 *
1954 * @return array|bool
1955 * @since 3.1.8
1956 */
1957 public function trash_marked_funnels($payload)
1958 {
1959 $funnel_controller = Module::instance();
1960
1961 return $funnel_controller->trash_marked_funnels($payload);
1962 }
1963
1964
1965 /**
1966 * Convert a regular funnel to a Store Checkout funnel.
1967 *
1968 * Sets the funnel type to store_checkout, removes any landing steps
1969 * permanently, and ensures the checkout step is first.
1970 *
1971 * @param WP_REST_Request $request
1972 * @return WP_REST_Response|WP_Error
1973 * @since 3.10.6
1974 */
1975 public function convert_to_store_checkout( $request ) {
1976 $funnel_id = absint( $request['funnel_id'] );
1977
1978 if ( ! $funnel_id ) {
1979 return new WP_Error(
1980 'rest_invalid_funnel_id',
1981 __( 'Invalid funnel ID.', 'wpfnl' ),
1982 array( 'status' => 400 )
1983 );
1984 }
1985
1986 // Set funnel type to store_checkout.
1987 update_post_meta( $funnel_id, '_wpfnl_funnel_type', 'store_checkout' );
1988
1989 // Get current steps order.
1990 $steps_order = get_post_meta( $funnel_id, '_steps_order', true );
1991 if ( ! is_array( $steps_order ) ) {
1992 $steps_order = array();
1993 }
1994
1995 // Find and permanently delete all landing steps.
1996 $new_steps_order = array();
1997 foreach ( $steps_order as $step ) {
1998 if ( isset( $step['step_type'] ) && 'landing' === $step['step_type'] ) {
1999 wp_delete_post( absint( $step['id'] ), true );
2000 } else {
2001 $new_steps_order[] = $step;
2002 }
2003 }
2004
2005 // Sort so checkout step comes first.
2006 usort( $new_steps_order, function( $a, $b ) {
2007 $priority = array( 'checkout' => 0, 'thankyou' => 1, 'upsell' => 2, 'downsell' => 3 );
2008 $a_priority = isset( $priority[ $a['step_type'] ] ) ? $priority[ $a['step_type'] ] : 99;
2009 $b_priority = isset( $priority[ $b['step_type'] ] ) ? $priority[ $b['step_type'] ] : 99;
2010 return $a_priority - $b_priority;
2011 } );
2012
2013 // Re-index to avoid serialization issues.
2014 $new_steps_order = array_values( $new_steps_order );
2015
2016 // Persist both step meta keys that the codebase reads from.
2017 update_post_meta( $funnel_id, '_steps_order', $new_steps_order );
2018 update_post_meta( $funnel_id, '_steps', $new_steps_order );
2019
2020 // Update _first_step to the checkout step.
2021 foreach ( $new_steps_order as $step ) {
2022 if ( isset( $step['step_type'] ) && 'checkout' === $step['step_type'] ) {
2023 Wpfnl_functions::update_funnel_first_step( $funnel_id, $step['id'] );
2024 break;
2025 }
2026 }
2027
2028 // Remove landing step nodes from funnel canvas data and clean up dangling connections.
2029 $funnel_data = get_post_meta( $funnel_id, '_funnel_data', true );
2030 if ( is_array( $funnel_data ) && isset( $funnel_data['drawflow']['Home']['data'] ) ) {
2031 // Collect landing node IDs first.
2032 $landing_node_ids = array();
2033 foreach ( $funnel_data['drawflow']['Home']['data'] as $node_id => $node ) {
2034 if ( isset( $node['data']['step_type'] ) && 'landing' === $node['data']['step_type'] ) {
2035 $landing_node_ids[] = (string) $node_id;
2036 }
2037 }
2038
2039 // Remove landing nodes.
2040 foreach ( $landing_node_ids as $landing_node_id ) {
2041 unset( $funnel_data['drawflow']['Home']['data'][ $landing_node_id ] );
2042 }
2043
2044 // Clean up input/output connections in remaining nodes that referenced landing nodes.
2045 foreach ( $funnel_data['drawflow']['Home']['data'] as $node_id => &$node ) {
2046 // Clean inputs.
2047 if ( isset( $node['inputs'] ) && is_array( $node['inputs'] ) ) {
2048 foreach ( $node['inputs'] as $input_key => &$input ) {
2049 if ( isset( $input['connections'] ) && is_array( $input['connections'] ) ) {
2050 $input['connections'] = array_values( array_filter(
2051 $input['connections'],
2052 function( $conn ) use ( $landing_node_ids ) {
2053 return ! in_array( (string) $conn['node'], $landing_node_ids, true );
2054 }
2055 ) );
2056 }
2057 }
2058 unset( $input );
2059 }
2060 // Clean outputs.
2061 if ( isset( $node['outputs'] ) && is_array( $node['outputs'] ) ) {
2062 foreach ( $node['outputs'] as $output_key => &$output ) {
2063 if ( isset( $output['connections'] ) && is_array( $output['connections'] ) ) {
2064 $output['connections'] = array_values( array_filter(
2065 $output['connections'],
2066 function( $conn ) use ( $landing_node_ids ) {
2067 return ! in_array( (string) $conn['node'], $landing_node_ids, true );
2068 }
2069 ) );
2070 }
2071 }
2072 unset( $output );
2073 }
2074 }
2075 unset( $node );
2076
2077 update_post_meta( $funnel_id, '_funnel_data', $funnel_data );
2078 }
2079
2080 // Return the checkout step ID so the frontend can use it immediately.
2081 $checkout_step_id = null;
2082 foreach ( $new_steps_order as $step ) {
2083 if ( isset( $step['step_type'] ) && 'checkout' === $step['step_type'] ) {
2084 $checkout_step_id = $step['id'];
2085 delete_post_meta( absint( $step['id'] ), '_wpfnl_checkout_products' );
2086 break;
2087 }
2088 }
2089
2090 return rest_ensure_response( array(
2091 'success' => true,
2092 'message' => __( 'Funnel converted to Store Checkout successfully.', 'wpfnl' ),
2093 'checkout_step_id' => $checkout_step_id,
2094 ) );
2095 }
2096
2097 }
2098