PluginProbe
CartFlows – Funnel Builder & Checkout Plugin for WooCommerce / 3.1.4
CartFlows – Funnel Builder & Checkout Plugin for WooCommerce v3.1.4
3.3.0 3.2.1 3.2.0 3.1.4 3.1.3 3.1.2 3.1.1 3.1.0 3.0.1 trunk 1.0.4 1.1.0 1.1.0.1 1.1.1 1.1.10 1.1.11 1.1.12 1.1.13 1.1.14 1.1.15 1.1.16 1.1.17 1.1.18 1.1.19 1.1.2 All 162 releases
cartflows / abilities / class-cartflows-ability-runtime.php

class-cartflows-ability-runtime.php in CartFlows – Funnel Builder & Checkout Plugin for WooCommerce 3.1.4, at abilities/class-cartflows-ability-runtime.php

3,475 lines 111.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * CartFlows Ability Runtime.
4 *
5 * Execute callbacks and helper methods for all registered CartFlows abilities.
6 *
7 * @package CartFlows
8 */
9
10 // Exit if accessed directly.
11 if ( ! defined( 'ABSPATH' ) ) {
12 exit;
13 }
14
15 use CartflowsAdmin\AdminCore\Inc\AdminHelper;
16 use CartflowsAdmin\AdminCore\Inc\FlowMeta;
17 use CartflowsAdmin\AdminCore\Inc\StepMeta;
18
19 /**
20 * Class Cartflows_Ability_Runtime
21 *
22 * Handles category registration, ability registration, and all execute callbacks.
23 */
24 class Cartflows_Ability_Runtime {
25
26 /**
27 * Parsed input values for the current execution.
28 *
29 * @var array|false
30 */
31 protected $input = false;
32
33 // ============================================================
34 // Registration
35 // ============================================================
36
37 /**
38 * Register the CartFlows ability category.
39 *
40 * Hooked to: wp_abilities_api_categories_init
41 *
42 * @return void
43 */
44 public function register_categories() {
45
46 wp_register_ability_category(
47 'cartflows',
48 array(
49 'label' => __( 'CartFlows', 'cartflows' ),
50 'description' => __( 'Abilities for CartFlows — funnel and step management.', 'cartflows' ),
51 )
52 );
53 }
54
55 /**
56 * Register all CartFlows abilities.
57 *
58 * Hooked to: wp_abilities_api_init
59 *
60 * @return void
61 */
62 public function register() {
63
64 $abilities = Cartflows_Ability_Config::get_abilities();
65
66 foreach ( $abilities as $ability_name => $ability ) {
67 wp_register_ability(
68 $ability_name,
69 array(
70 'label' => $ability['label'],
71 'description' => $ability['description'],
72 'category' => $ability['category'],
73 'input_schema' => $ability['input_schema'],
74 'output_schema' => $ability['output_schema'],
75 'execute_callback' => $ability['execute_callback'],
76 'permission_callback' => $ability['permission_callback'],
77 'meta' => $ability['meta'],
78 )
79 );
80 }
81 }
82
83 // ============================================================
84 // Execute Callbacks — Flow Read
85 // ============================================================
86
87 /**
88 * List funnels with pagination and optional status/search filters.
89 *
90 * @param array $input Validated input.
91 * @return array
92 */
93 public function list_flows( $input ) {
94
95 try {
96 $this->init( $input, CARTFLOWS_ABILITY_API_NAMESPACE . 'list-flows' );
97
98 $status = $this->input_get( 'status' );
99 $search = $this->input_get( 'search' );
100 $page = $this->clamp_page( $this->input_get( 'paged' ) );
101 $per_page = $this->clamp_per_page( $this->input_get( 'per_page' ) );
102
103 // Exclude the store checkout flow.
104 $store_checkout_id = intval( \Cartflows_Helper::get_global_setting( '_cartflows_store_checkout' ) );
105
106 $args = array(
107 'post_type' => CARTFLOWS_FLOW_POST_TYPE,
108 'post_status' => $status,
109 'posts_per_page' => $per_page,
110 'paged' => $page,
111 'orderby' => 'ID',
112 'order' => 'DESC',
113 );
114
115 if ( ! empty( $search ) ) {
116 $args['s'] = $search;
117 }
118
119 if ( $store_checkout_id > 0 ) {
120 $args['post__not_in'] = array( $store_checkout_id ); //phpcs:ignore WordPressVIPMinimum.Performance.WPQueryParams.PostNotIn_post__not_in
121 }
122
123 $query = new \WP_Query( $args );
124
125 $flows = array();
126
127 if ( $query->have_posts() ) {
128 while ( $query->have_posts() ) {
129 $query->the_post();
130 global $post;
131
132 $flows[] = array(
133 'id' => intval( $post->ID ),
134 'title' => esc_html( $post->post_title ),
135 'status' => esc_html( $post->post_status ),
136 'date_created' => esc_html( $post->post_date ),
137 'date_modified' => esc_html( $post->post_modified ),
138 'url_view' => esc_url( get_permalink( $post->ID ) ),
139 'url_edit' => esc_url( admin_url( 'admin.php?page=cartflows&path=flows&action=wcf-edit-flow&flow_id=' . $post->ID ) ),
140 'flow_test_mode' => ( 'yes' === wcf()->options->get_flow_meta_value( $post->ID, 'wcf-testing' ) ),
141 );
142 }
143 wp_reset_postdata();
144 }
145
146 $total = intval( $query->found_posts );
147 $total_pages = ( $per_page > 0 ) ? intval( ceil( $total / $per_page ) ) : 1;
148
149 return array(
150 'flows' => $flows,
151 'total' => $total,
152 'total_pages' => $total_pages,
153 'page' => $page,
154 );
155
156 } catch ( \Exception $e ) {
157 return $this->error( $e );
158 }
159 }
160
161 /**
162 * Get a single funnel by ID with full step list, settings, and links.
163 *
164 * @param array $input Validated input.
165 * @return array
166 * @throws \Exception On failure.
167 */
168 public function get_flow( $input ) {
169
170 try {
171 $this->init( $input, CARTFLOWS_ABILITY_API_NAMESPACE . 'get-flow' );
172
173 $flow_id = $this->input_get( 'id' );
174
175 if ( 0 === $flow_id ) {
176 throw new \Exception( esc_html__( 'Invalid funnel ID.', 'cartflows' ) );
177 }
178
179 if ( CARTFLOWS_FLOW_POST_TYPE !== get_post_type( $flow_id ) ) {
180 throw new \Exception( esc_html__( 'Funnel not found.', 'cartflows' ) );
181 }
182
183 $meta_options = AdminHelper::get_flow_meta_options( $flow_id );
184 $steps_raw = AdminHelper::prepare_step_data( $flow_id, $meta_options );
185 $settings_data = FlowMeta::get_meta_settings( $flow_id );
186
187 $steps = array();
188 if ( is_array( $steps_raw ) ) {
189 foreach ( $steps_raw as $step ) {
190 $step_id = isset( $step['id'] ) ? intval( $step['id'] ) : 0;
191 $steps[] = array(
192 'id' => $step_id,
193 'title' => esc_html( isset( $step['title'] ) ? $step['title'] : get_the_title( $step_id ) ),
194 'type' => esc_html( isset( $step['type'] ) ? $step['type'] : '' ),
195 'status' => esc_html( get_post_status( $step_id ) ),
196 'is_disabled' => ( 'yes' === get_post_meta( $step_id, 'wcf-disable-step', true ) ),
197 'url' => esc_url( get_permalink( $step_id ) ),
198 );
199 }
200 }
201
202 return array(
203 'id' => intval( $flow_id ),
204 'title' => esc_html( get_the_title( $flow_id ) ),
205 'slug' => esc_html( get_post_field( 'post_name', $flow_id, 'edit' ) ),
206 'status' => esc_html( get_post_status( $flow_id ) ),
207 'url_view' => esc_url( get_permalink( $flow_id ) ),
208 'url_edit' => esc_url( admin_url( 'admin.php?page=cartflows&path=flows&action=wcf-edit-flow&flow_id=' . $flow_id ) ),
209 'steps' => $steps,
210 'settings_data' => $settings_data,
211 );
212
213 } catch ( \Exception $e ) {
214 return $this->error( $e );
215 }
216 }
217
218 /**
219 * Get a single step by ID with type, settings, and page-builder links.
220 *
221 * @param array $input Validated input.
222 * @return array
223 * @throws \Exception On failure.
224 */
225 public function get_step( $input ) {
226
227 try {
228 $this->init( $input, CARTFLOWS_ABILITY_API_NAMESPACE . 'get-step' );
229
230 $step_id = $this->input_get( 'id' );
231
232 if ( 0 === $step_id ) {
233 throw new \Exception( esc_html__( 'Invalid step ID.', 'cartflows' ) );
234 }
235
236 if ( CARTFLOWS_STEP_POST_TYPE !== get_post_type( $step_id ) ) {
237 throw new \Exception( esc_html__( 'Step not found.', 'cartflows' ) );
238 }
239
240 $meta_options = AdminHelper::get_step_meta_options( $step_id );
241 $step_type = isset( $meta_options['type'] ) ? $meta_options['type'] : '';
242 $flow_id = intval( get_post_meta( $step_id, 'wcf-flow-id', true ) );
243
244 $settings_data = StepMeta::get_meta_settings( $step_id, $step_type );
245
246 return array(
247 'id' => intval( $step_id ),
248 'title' => esc_html( get_the_title( $step_id ) ),
249 'type' => esc_html( $step_type ),
250 'flow_id' => $flow_id,
251 'flow_title' => esc_html( get_the_title( $flow_id ) ),
252 'url_view' => esc_url( get_permalink( $step_id ) ),
253 'url_edit' => esc_url( get_edit_post_link( $step_id, 'edit' ) ),
254 'url_page_builder' => esc_url( (string) \Cartflows_Helper::get_page_builder_edit_link( $step_id ) ),
255 'settings_data' => isset( $settings_data['settings'] ) ? $settings_data['settings'] : array(),
256 );
257
258 } catch ( \Exception $e ) {
259 return $this->error( $e );
260 }
261 }
262
263 // ============================================================
264 // Execute Callbacks — Analytics
265 // ============================================================
266
267 /**
268 * Get flow analytics: revenue and order data for a date range.
269 *
270 * @param array $input Validated input.
271 * @return array
272 * @throws \Exception On failure.
273 */
274 public function get_flow_stats( $input ) {
275
276 try {
277 $this->init( $input, CARTFLOWS_ABILITY_API_NAMESPACE . 'get-flow-stats' );
278
279 if ( ! wcf()->is_woo_active ) {
280 throw new \Exception( esc_html__( 'WooCommerce is required for analytics.', 'cartflows' ) );
281 }
282
283 $date_from = $this->input_get( 'date_from' );
284 $date_to = $this->input_get( 'date_to' );
285 $flow_id_raw = $this->input_get( 'flow_id' );
286 $flow_id = intval( $flow_id_raw );
287
288 // Validate flow if specified.
289 if ( $flow_id > 0 && CARTFLOWS_FLOW_POST_TYPE !== get_post_type( $flow_id ) ) {
290 throw new \Exception( esc_html__( 'Invalid funnel ID.', 'cartflows' ) );
291 }
292
293 $flow_id_param = ( $flow_id > 0 ) ? $flow_id : '';
294
295 $earnings_data = AdminHelper::get_earnings( $date_from, $date_to, $flow_id_param, 'funnels', '' );
296
297 // Collect recent orders (last 4 CartFlows orders).
298 $recent_orders_raw = wc_get_orders(
299 array(
300 'limit' => 4,
301 'orderby' => 'date',
302 'order' => 'DESC',
303 'meta_key' => '_wcf_flow_id', // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key
304 'meta_compare' => 'EXISTS',
305 )
306 );
307
308 $recent_orders = array();
309 foreach ( $recent_orders_raw as $order ) {
310 $recent_orders[] = array(
311 'order_id' => intval( $order->get_id() ),
312 'customer_name' => esc_html( $order->get_billing_first_name() . ' ' . $order->get_billing_last_name() ),
313 'customer_email' => sanitize_email( $order->get_billing_email() ),
314 'order_total' => esc_html( get_woocommerce_currency_symbol( $order->get_currency() ) . $order->get_total() ),
315 'order_status' => esc_html( ucfirst( $order->get_status() ) ),
316 'order_date' => esc_html( wc_format_datetime( $order->get_date_created(), 'Y-m-d' ) ),
317 );
318 }
319
320 return array(
321 'flow_stats' => is_array( $earnings_data ) ? $earnings_data : array(),
322 'recent_orders' => $recent_orders,
323 );
324
325 } catch ( \Exception $e ) {
326 return $this->error( $e );
327 }
328 }
329
330 // ============================================================
331 // Execute Callbacks — Flow Lifecycle
332 // ============================================================
333
334 /**
335 * Publish a funnel and all its steps.
336 *
337 * @param array $input Validated input.
338 * @return array
339 * @throws \Exception On failure.
340 */
341 public function publish_flow( $input ) {
342
343 try {
344 $this->init( $input, CARTFLOWS_ABILITY_API_NAMESPACE . 'publish-flow' );
345
346 $flow_id = $this->input_get( 'id' );
347
348 if ( 0 === $flow_id ) {
349 throw new \Exception( esc_html__( 'Invalid funnel ID.', 'cartflows' ) );
350 }
351
352 if ( CARTFLOWS_FLOW_POST_TYPE !== get_post_type( $flow_id ) ) {
353 throw new \Exception( esc_html__( 'Funnel not found.', 'cartflows' ) );
354 }
355
356 $this->set_flow_status( $flow_id, 'publish' );
357
358 return array(
359 'id' => intval( $flow_id ),
360 'status' => 'publish',
361 'message' => esc_html__( 'Funnel published successfully.', 'cartflows' ),
362 );
363
364 } catch ( \Exception $e ) {
365 return $this->error( $e );
366 }
367 }
368
369 /**
370 * Unpublish (draft) a funnel and all its steps.
371 *
372 * @param array $input Validated input.
373 * @return array
374 * @throws \Exception On failure.
375 */
376 public function unpublish_flow( $input ) {
377
378 try {
379 $this->init( $input, CARTFLOWS_ABILITY_API_NAMESPACE . 'unpublish-flow' );
380
381 $flow_id = $this->input_get( 'id' );
382
383 if ( 0 === $flow_id ) {
384 throw new \Exception( esc_html__( 'Invalid funnel ID.', 'cartflows' ) );
385 }
386
387 if ( CARTFLOWS_FLOW_POST_TYPE !== get_post_type( $flow_id ) ) {
388 throw new \Exception( esc_html__( 'Funnel not found.', 'cartflows' ) );
389 }
390
391 $this->set_flow_status( $flow_id, 'draft' );
392
393 return array(
394 'id' => intval( $flow_id ),
395 'status' => 'draft',
396 'message' => esc_html__( 'Funnel unpublished (set to draft).', 'cartflows' ),
397 );
398
399 } catch ( \Exception $e ) {
400 return $this->error( $e );
401 }
402 }
403
404 /**
405 * Clone a funnel with all its steps and settings.
406 *
407 * @param array $input Validated input.
408 * @return array
409 * @throws \Exception On failure.
410 */
411 public function clone_flow( $input ) {
412
413 try {
414 $this->init( $input, CARTFLOWS_ABILITY_API_NAMESPACE . 'clone-flow' );
415
416 $flow_id = $this->input_get( 'id' );
417
418 if ( 0 === $flow_id ) {
419 throw new \Exception( esc_html__( 'Invalid funnel ID.', 'cartflows' ) );
420 }
421
422 $post = get_post( $flow_id );
423
424 if ( ! $post || CARTFLOWS_FLOW_POST_TYPE !== $post->post_type ) {
425 throw new \Exception( esc_html__( 'Funnel not found.', 'cartflows' ) );
426 }
427
428 global $wpdb;
429
430 $current_user = wp_get_current_user();
431 $new_post_author = intval( $current_user->ID );
432
433 // Insert the cloned flow post.
434 $new_flow_id = wp_insert_post(
435 array(
436 'comment_status' => $post->comment_status,
437 'ping_status' => $post->ping_status,
438 'post_author' => $new_post_author,
439 'post_content' => $post->post_content,
440 'post_excerpt' => $post->post_excerpt,
441 'post_name' => $post->post_name,
442 'post_parent' => $post->post_parent,
443 'post_password' => $post->post_password,
444 'post_status' => $post->post_status,
445 'post_title' => $post->post_title . ' Clone',
446 'post_type' => $post->post_type,
447 'to_ping' => $post->to_ping,
448 'menu_order' => $post->menu_order,
449 )
450 );
451
452 if ( is_wp_error( $new_flow_id ) || ! $new_flow_id ) {
453 throw new \Exception( esc_html__( 'Failed to create funnel clone.', 'cartflows' ) );
454 }
455
456 // Copy taxonomies.
457 foreach ( get_object_taxonomies( $post->post_type ) as $taxonomy ) {
458 $post_terms = wp_get_object_terms( $flow_id, $taxonomy, array( 'fields' => 'slugs' ) );
459 wp_set_object_terms( $new_flow_id, $post_terms, $taxonomy, false );
460 }
461
462 // Copy post meta.
463 $post_meta_infos = $wpdb->get_results( // phpcs:ignore WordPress.DB.DirectDatabaseQuery
464 $wpdb->prepare( "SELECT meta_key, meta_value FROM $wpdb->postmeta WHERE post_id=%d", $flow_id )
465 );
466
467 if ( ! empty( $post_meta_infos ) ) {
468 $sql_query_sel = array();
469 foreach ( $post_meta_infos as $meta_info ) {
470 if ( '_wp_old_slug' === $meta_info->meta_key ) {
471 continue;
472 }
473 $sql_query_sel[] = $wpdb->prepare( '( %d, %s, %s )', $new_flow_id, $meta_info->meta_key, $meta_info->meta_value );
474 }
475 if ( ! empty( $sql_query_sel ) ) {
476 $wpdb->query( "INSERT INTO $wpdb->postmeta (post_id, meta_key, meta_value) VALUES " . implode( ',', $sql_query_sel ) ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery
477 }
478 }
479
480 // Clone all steps.
481 $flow_steps = get_post_meta( $flow_id, 'wcf-steps', true );
482 $new_flow_steps = array();
483
484 update_post_meta( $new_flow_id, 'wcf-steps', array() );
485
486 if ( is_array( $flow_steps ) && ! empty( $flow_steps ) ) {
487 foreach ( $flow_steps as $step_data ) {
488 $step_id = intval( $step_data['id'] );
489 $step_object = get_post( $step_id );
490 $step_type = get_post_meta( $step_id, 'wcf-step-type', true );
491
492 if ( ! $step_object ) {
493 continue;
494 }
495
496 $new_step_id = wp_insert_post(
497 array(
498 'comment_status' => $step_object->comment_status,
499 'ping_status' => $step_object->ping_status,
500 'post_author' => $new_post_author,
501 'post_content' => $step_object->post_content,
502 'post_excerpt' => $step_object->post_excerpt,
503 'post_name' => $step_object->post_name,
504 'post_parent' => $step_object->post_parent,
505 'post_password' => $step_object->post_password,
506 'post_status' => $step_object->post_status,
507 'post_title' => $step_object->post_title,
508 'post_type' => $step_object->post_type,
509 'to_ping' => $step_object->to_ping,
510 'menu_order' => $step_object->menu_order,
511 )
512 );
513
514 if ( is_wp_error( $new_step_id ) || ! $new_step_id ) {
515 continue;
516 }
517
518 $step_meta_infos = $wpdb->get_results( // phpcs:ignore WordPress.DB.DirectDatabaseQuery
519 $wpdb->prepare( "SELECT meta_key, meta_value FROM $wpdb->postmeta WHERE post_id=%d", $step_id )
520 );
521
522 if ( ! empty( $step_meta_infos ) ) {
523 $step_sql_sel = array();
524 foreach ( $step_meta_infos as $meta_info ) {
525 $step_sql_sel[] = $wpdb->prepare( '( %d, %s, %s )', $new_step_id, $meta_info->meta_key, $meta_info->meta_value );
526 }
527 if ( ! empty( $step_sql_sel ) ) {
528 $wpdb->query( "INSERT INTO $wpdb->postmeta (post_id, meta_key, meta_value) VALUES " . implode( ',', $step_sql_sel ) ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery
529 }
530 }
531
532 update_post_meta( $new_step_id, 'wcf-flow-id', $new_flow_id );
533 update_post_meta( $new_step_id, 'wcf-step-type', $step_type );
534 wp_set_object_terms( $new_step_id, $step_type, CARTFLOWS_TAXONOMY_STEP_TYPE );
535 wp_set_object_terms( $new_step_id, 'flow-' . $new_flow_id, CARTFLOWS_TAXONOMY_STEP_FLOW );
536
537 $new_flow_steps[] = array(
538 'id' => intval( $new_step_id ),
539 'title' => esc_html( $step_object->post_title ),
540 'type' => esc_html( $step_type ),
541 );
542 }
543 }
544
545 update_post_meta( $new_flow_id, 'wcf-steps', $new_flow_steps );
546 AdminHelper::clear_cache();
547
548 return array(
549 'id' => intval( $new_flow_id ),
550 'source_id' => intval( $flow_id ),
551 'title' => esc_html( get_the_title( $new_flow_id ) ),
552 'url_edit' => esc_url( admin_url( 'admin.php?page=cartflows&path=flows&action=wcf-edit-flow&flow_id=' . $new_flow_id ) ),
553 'steps_count' => count( $new_flow_steps ),
554 'message' => esc_html__( 'Funnel cloned successfully.', 'cartflows' ),
555 );
556
557 } catch ( \Exception $e ) {
558 return $this->error( $e );
559 }
560 }
561
562 /**
563 * Move a funnel and all its steps to trash.
564 *
565 * @param array $input Validated input.
566 * @return array
567 * @throws \Exception On failure.
568 */
569 public function trash_flow( $input ) {
570
571 try {
572 $this->init( $input, CARTFLOWS_ABILITY_API_NAMESPACE . 'trash-flow' );
573
574 $flow_id = $this->input_get( 'id' );
575
576 if ( 0 === $flow_id ) {
577 throw new \Exception( esc_html__( 'Invalid funnel ID.', 'cartflows' ) );
578 }
579
580 if ( CARTFLOWS_FLOW_POST_TYPE !== get_post_type( $flow_id ) ) {
581 throw new \Exception( esc_html__( 'Funnel not found.', 'cartflows' ) );
582 }
583
584 // Trash all steps.
585 $steps = get_post_meta( $flow_id, 'wcf-steps', true );
586 if ( is_array( $steps ) ) {
587 foreach ( $steps as $step ) {
588 wp_trash_post( intval( $step['id'] ) );
589 }
590 }
591
592 // Trash the flow taxonomy term.
593 $term_data = term_exists( 'flow-' . $flow_id, CARTFLOWS_TAXONOMY_STEP_FLOW );
594 if ( is_array( $term_data ) ) {
595 wp_delete_term( intval( $term_data['term_id'] ), CARTFLOWS_TAXONOMY_STEP_FLOW );
596 }
597
598 // Trash the flow.
599 wp_trash_post( $flow_id );
600
601 return array(
602 'id' => intval( $flow_id ),
603 'status' => 'trash',
604 'message' => esc_html__( 'Funnel moved to trash.', 'cartflows' ),
605 );
606
607 } catch ( \Exception $e ) {
608 return $this->error( $e );
609 }
610 }
611
612 /**
613 * Restore a trashed funnel and all its steps.
614 *
615 * @param array $input Validated input.
616 * @return array
617 * @throws \Exception On failure.
618 */
619 public function restore_flow( $input ) {
620
621 try {
622 $this->init( $input, CARTFLOWS_ABILITY_API_NAMESPACE . 'restore-flow' );
623
624 $flow_id = $this->input_get( 'id' );
625
626 if ( 0 === $flow_id ) {
627 throw new \Exception( esc_html__( 'Invalid funnel ID.', 'cartflows' ) );
628 }
629
630 if ( CARTFLOWS_FLOW_POST_TYPE !== get_post_type( $flow_id ) ) {
631 throw new \Exception( esc_html__( 'Funnel not found.', 'cartflows' ) );
632 }
633
634 // Restore all steps.
635 $steps = get_post_meta( $flow_id, 'wcf-steps', true );
636 if ( is_array( $steps ) ) {
637 foreach ( $steps as $step ) {
638 wp_untrash_post( intval( $step['id'] ) );
639 }
640 }
641
642 // Restore the flow.
643 wp_untrash_post( $flow_id );
644
645 return array(
646 'id' => intval( $flow_id ),
647 'status' => 'publish',
648 'message' => esc_html__( 'Funnel restored from trash.', 'cartflows' ),
649 );
650
651 } catch ( \Exception $e ) {
652 return $this->error( $e );
653 }
654 }
655
656 // ============================================================
657 // Execute Callbacks — Flow Update
658 // ============================================================
659
660 /**
661 * Update a funnel's title and/or slug.
662 *
663 * @param array $input Validated input.
664 * @return array
665 * @throws \Exception On failure.
666 */
667 public function update_flow( $input ) {
668
669 try {
670 $this->init( $input, CARTFLOWS_ABILITY_API_NAMESPACE . 'update-flow' );
671
672 $flow_id = $this->input_get( 'id' );
673 $title = $this->input_get( 'title' );
674 $slug = $this->input_get( 'slug' );
675
676 if ( 0 === $flow_id ) {
677 throw new \Exception( esc_html__( 'Invalid funnel ID.', 'cartflows' ) );
678 }
679
680 if ( CARTFLOWS_FLOW_POST_TYPE !== get_post_type( $flow_id ) ) {
681 throw new \Exception( esc_html__( 'Funnel not found.', 'cartflows' ) );
682 }
683
684 if ( empty( $title ) ) {
685 throw new \Exception( esc_html__( 'Funnel title cannot be empty.', 'cartflows' ) );
686 }
687
688 $post_data = array(
689 'ID' => $flow_id,
690 'post_title' => sanitize_text_field( $title ),
691 );
692
693 if ( ! empty( $slug ) ) {
694 $post_data['post_name'] = sanitize_title( $slug );
695 }
696
697 $result = wp_update_post( $post_data );
698
699 if ( is_wp_error( $result ) ) {
700 throw new \Exception( esc_html__( 'Failed to update funnel.', 'cartflows' ) );
701 }
702
703 return array(
704 'id' => intval( $flow_id ),
705 'title' => esc_html( get_the_title( $flow_id ) ),
706 'slug' => esc_html( get_post_field( 'post_name', $flow_id, 'edit' ) ),
707 'url_edit' => esc_url( admin_url( 'admin.php?page=cartflows&path=flows&action=wcf-edit-flow&flow_id=' . $flow_id ) ),
708 'message' => esc_html__( 'Funnel updated successfully.', 'cartflows' ),
709 );
710
711 } catch ( \Exception $e ) {
712 return $this->error( $e );
713 }
714 }
715
716 // ============================================================
717 // Execute Callbacks — Flow Relationships
718 // ============================================================
719
720 /**
721 * Reorder the steps within a funnel.
722 *
723 * @param array $input Validated input.
724 * @return array
725 * @throws \Exception On failure.
726 */
727 public function reorder_flow_steps( $input ) {
728
729 try {
730 $this->init( $input, CARTFLOWS_ABILITY_API_NAMESPACE . 'reorder-flow-steps' );
731
732 $flow_id = $this->input_get( 'flow_id' );
733 $step_ids = $this->input_get( 'step_ids' );
734
735 if ( 0 === $flow_id ) {
736 throw new \Exception( esc_html__( 'Invalid funnel ID.', 'cartflows' ) );
737 }
738
739 if ( CARTFLOWS_FLOW_POST_TYPE !== get_post_type( $flow_id ) ) {
740 throw new \Exception( esc_html__( 'Funnel not found.', 'cartflows' ) );
741 }
742
743 if ( empty( $step_ids ) || ! is_array( $step_ids ) ) {
744 throw new \Exception( esc_html__( 'Step IDs array is required.', 'cartflows' ) );
745 }
746
747 $step_ids = array_map( 'intval', $step_ids );
748
749 // Build a lookup of current steps.
750 $flow_steps = get_post_meta( $flow_id, 'wcf-steps', true );
751 $flow_steps_map = array();
752
753 if ( is_array( $flow_steps ) ) {
754 foreach ( $flow_steps as $value ) {
755 $flow_steps_map[ intval( $value['id'] ) ] = $value;
756 }
757 }
758
759 $new_flow_steps = array();
760
761 foreach ( $step_ids as $step_id ) {
762 $new_step_data = isset( $flow_steps_map[ $step_id ] ) ? $flow_steps_map[ $step_id ] : array();
763 $new_step_data['id'] = $step_id;
764 $new_step_data['title'] = esc_html( get_the_title( $step_id ) );
765 $new_step_data['type'] = esc_html( get_post_meta( $step_id, 'wcf-step-type', true ) );
766 $new_flow_steps[] = $new_step_data;
767 }
768
769 update_post_meta( $flow_id, 'wcf-steps', $new_flow_steps );
770
771 $ordered_steps = array();
772 foreach ( $new_flow_steps as $step ) {
773 $ordered_steps[] = array(
774 'id' => intval( $step['id'] ),
775 'title' => esc_html( $step['title'] ),
776 'type' => esc_html( $step['type'] ),
777 );
778 }
779
780 return array(
781 'flow_id' => intval( $flow_id ),
782 'steps' => $ordered_steps,
783 'message' => esc_html__( 'Funnel steps reordered successfully.', 'cartflows' ),
784 );
785
786 } catch ( \Exception $e ) {
787 return $this->error( $e );
788 }
789 }
790
791 // ============================================================
792 // Execute Callbacks — Flow Output
793 // ============================================================
794
795 /**
796 * Export one or more funnels as JSON data.
797 *
798 * @param array $input Validated input.
799 * @return array
800 * @throws \Exception On failure.
801 */
802 public function export_flow( $input ) {
803
804 try {
805 $this->init( $input, CARTFLOWS_ABILITY_API_NAMESPACE . 'export-flow' );
806
807 $flow_ids = $this->input_get( 'flow_ids' );
808
809 if ( empty( $flow_ids ) || ! is_array( $flow_ids ) ) {
810 throw new \Exception( esc_html__( 'At least one funnel ID is required.', 'cartflows' ) );
811 }
812
813 $flow_ids = array_map( 'intval', $flow_ids );
814
815 $exporter = \CartFlows_Importer::get_instance();
816 $flows = array();
817
818 foreach ( $flow_ids as $flow_id ) {
819 if ( $flow_id <= 0 ) {
820 continue;
821 }
822 if ( CARTFLOWS_FLOW_POST_TYPE !== get_post_type( $flow_id ) ) {
823 continue;
824 }
825 $flows[] = $exporter->get_flow_export_data( $flow_id );
826 }
827
828 if ( empty( $flows ) ) {
829 throw new \Exception( esc_html__( 'No valid funnel IDs provided for export.', 'cartflows' ) );
830 }
831
832 return array(
833 'flows' => $flows,
834 'export_count' => count( $flows ),
835 'message' => esc_html__( 'Funnel data exported successfully.', 'cartflows' ),
836 );
837
838 } catch ( \Exception $e ) {
839 return $this->error( $e );
840 }
841 }
842
843 // ============================================================
844 // Execute Callbacks — Step Lifecycle
845 // ============================================================
846
847 /**
848 * Clone a step within the same funnel.
849 *
850 * @param array $input Validated input.
851 * @return array
852 * @throws \Exception On failure.
853 */
854 public function clone_step( $input ) {
855
856 try {
857 $this->init( $input, CARTFLOWS_ABILITY_API_NAMESPACE . 'clone-step' );
858
859 $flow_id = $this->input_get( 'flow_id' );
860 $step_id = $this->input_get( 'step_id' );
861
862 if ( 0 === $flow_id || 0 === $step_id ) {
863 throw new \Exception( esc_html__( 'Invalid funnel or step ID.', 'cartflows' ) );
864 }
865
866 if ( get_post_type( $flow_id ) !== CARTFLOWS_FLOW_POST_TYPE ) {
867 throw new \Exception( esc_html__( 'Funnel not found.', 'cartflows' ) );
868 }
869
870 if ( get_post_type( $step_id ) !== CARTFLOWS_STEP_POST_TYPE ) {
871 throw new \Exception( esc_html__( 'Step not found.', 'cartflows' ) );
872 }
873
874 $step_object = get_post( $step_id );
875 $current_user = wp_get_current_user();
876 $new_post_author = intval( $current_user->ID );
877
878 global $wpdb;
879
880 // Insert the cloned step.
881 $new_step_id = wp_insert_post(
882 array(
883 'comment_status' => $step_object->comment_status,
884 'ping_status' => $step_object->ping_status,
885 'post_author' => $new_post_author,
886 'post_content' => $step_object->post_content,
887 'post_excerpt' => $step_object->post_excerpt,
888 'post_name' => $step_object->post_name,
889 'post_parent' => $step_object->post_parent,
890 'post_password' => $step_object->post_password,
891 'post_status' => $step_object->post_status,
892 'post_title' => $step_object->post_title . ' Clone',
893 'post_type' => $step_object->post_type,
894 'to_ping' => $step_object->to_ping,
895 'menu_order' => $step_object->menu_order,
896 )
897 );
898
899 if ( is_wp_error( $new_step_id ) || ! $new_step_id ) {
900 throw new \Exception( esc_html__( 'Failed to create step clone.', 'cartflows' ) );
901 }
902
903 // Copy taxonomies.
904 foreach ( get_object_taxonomies( $step_object->post_type ) as $taxonomy ) {
905 $post_terms = wp_get_object_terms( $step_id, $taxonomy, array( 'fields' => 'slugs' ) );
906 wp_set_object_terms( $new_step_id, $post_terms, $taxonomy, false );
907 }
908
909 // Copy step meta.
910 $step_meta_infos = $wpdb->get_results( // phpcs:ignore WordPress.DB.DirectDatabaseQuery
911 $wpdb->prepare( "SELECT meta_key, meta_value FROM $wpdb->postmeta WHERE post_id=%d", $step_id )
912 );
913
914 if ( ! empty( $step_meta_infos ) ) {
915 $step_sql_sel = array();
916 foreach ( $step_meta_infos as $meta_info ) {
917 if ( '_wp_old_slug' === $meta_info->meta_key ) {
918 continue;
919 }
920 $step_sql_sel[] = $wpdb->prepare( '( %d, %s, %s )', $new_step_id, $meta_info->meta_key, $meta_info->meta_value );
921 }
922 if ( ! empty( $step_sql_sel ) ) {
923 $wpdb->query( "INSERT INTO $wpdb->postmeta (post_id, meta_key, meta_value) VALUES " . implode( ',', $step_sql_sel ) ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery
924 }
925 }
926
927 $step_type = get_post_meta( $step_id, 'wcf-step-type', true );
928
929 update_post_meta( $new_step_id, 'wcf-flow-id', $flow_id );
930 update_post_meta( $new_step_id, 'wcf-step-type', $step_type );
931 wp_set_object_terms( $new_step_id, $step_type, CARTFLOWS_TAXONOMY_STEP_TYPE );
932 wp_set_object_terms( $new_step_id, 'flow-' . $flow_id, CARTFLOWS_TAXONOMY_STEP_FLOW );
933
934 // Append the new step to the flow's step list.
935 $flow_steps = get_post_meta( $flow_id, 'wcf-steps', true );
936 if ( ! is_array( $flow_steps ) ) {
937 $flow_steps = array();
938 }
939
940 $flow_steps[] = array(
941 'id' => intval( $new_step_id ),
942 'title' => esc_html( $step_object->post_title ),
943 'type' => esc_html( $step_type ),
944 );
945
946 if ( method_exists( '\Cartflows_Helper', 'get_instance' ) ) {
947 $flow_steps = \Cartflows_Helper::get_instance()->maybe_update_flow_steps( $flow_id, $flow_steps );
948 }
949
950 update_post_meta( $flow_id, 'wcf-steps', $flow_steps );
951 AdminHelper::clear_cache();
952
953 return array(
954 'id' => intval( $new_step_id ),
955 'source_id' => intval( $step_id ),
956 'flow_id' => intval( $flow_id ),
957 'title' => esc_html( get_the_title( $new_step_id ) ),
958 'type' => esc_html( $step_type ),
959 'message' => esc_html__( 'Step cloned successfully.', 'cartflows' ),
960 );
961
962 } catch ( \Exception $e ) {
963 return $this->error( $e );
964 }
965 }
966
967 // ============================================================
968 // Execute Callbacks — Step Update
969 // ============================================================
970
971 /**
972 * Rename a CartFlows step.
973 *
974 * @param array $input Validated input.
975 * @return array
976 * @throws \Exception On failure.
977 */
978 public function update_step_title( $input ) {
979
980 try {
981 $this->init( $input, CARTFLOWS_ABILITY_API_NAMESPACE . 'update-step-title' );
982
983 $step_id = $this->input_get( 'step_id' );
984 $title = $this->input_get( 'title' );
985
986 if ( 0 === $step_id ) {
987 throw new \Exception( esc_html__( 'Invalid step ID.', 'cartflows' ) );
988 }
989
990 if ( get_post_type( $step_id ) !== CARTFLOWS_STEP_POST_TYPE ) {
991 throw new \Exception( esc_html__( 'Step not found.', 'cartflows' ) );
992 }
993
994 if ( empty( $title ) ) {
995 throw new \Exception( esc_html__( 'Step title cannot be empty.', 'cartflows' ) );
996 }
997
998 $result = wp_update_post(
999 array(
1000 'ID' => $step_id,
1001 'post_title' => sanitize_text_field( $title ),
1002 )
1003 );
1004
1005 if ( is_wp_error( $result ) ) {
1006 throw new \Exception( esc_html__( 'Failed to update step title.', 'cartflows' ) );
1007 }
1008
1009 return array(
1010 'id' => intval( $step_id ),
1011 'title' => esc_html( get_the_title( $step_id ) ),
1012 'message' => esc_html__( 'Step title updated successfully.', 'cartflows' ),
1013 );
1014
1015 } catch ( \Exception $e ) {
1016 return $this->error( $e );
1017 }
1018 }
1019
1020 // ============================================================
1021 // Execute Callbacks — Admin / Settings Read
1022 // ============================================================
1023
1024 /**
1025 * Return CartFlows general settings (page builder, checkout, flags).
1026 *
1027 * @param array $input Validated input.
1028 * @return array
1029 * @throws \Exception On failure.
1030 */
1031 public function get_general_settings( $input ) {
1032
1033 try {
1034 $this->init( $input, CARTFLOWS_ABILITY_API_NAMESPACE . 'get-general-settings' );
1035
1036 $settings = \Cartflows_Helper::get_common_settings();
1037
1038 return array(
1039 'default_page_builder' => esc_html( isset( $settings['default_page_builder'] ) ? $settings['default_page_builder'] : '' ),
1040 'global_checkout' => esc_html( isset( $settings['global_checkout'] ) ? $settings['global_checkout'] : '' ),
1041 'override_global_checkout' => esc_html( isset( $settings['override_global_checkout'] ) ? $settings['override_global_checkout'] : '' ),
1042 'override_store_order_pay' => esc_html( isset( $settings['override_store_order_pay'] ) ? $settings['override_store_order_pay'] : '' ),
1043 'disallow_indexing' => esc_html( isset( $settings['disallow_indexing'] ) ? $settings['disallow_indexing'] : '' ),
1044 );
1045
1046 } catch ( \Exception $e ) {
1047 return $this->error( $e );
1048 }
1049 }
1050
1051 /**
1052 * Return the flow configured as the CartFlows store checkout.
1053 *
1054 * @param array $input Validated input.
1055 * @return array
1056 * @throws \Exception On failure.
1057 */
1058 public function get_store_checkout( $input ) {
1059
1060 try {
1061 $this->init( $input, CARTFLOWS_ABILITY_API_NAMESPACE . 'get-store-checkout' );
1062
1063 $flow_id = intval( \Cartflows_Helper::get_global_setting( '_cartflows_store_checkout' ) );
1064
1065 if ( $flow_id > 0 && CARTFLOWS_FLOW_POST_TYPE === get_post_type( $flow_id ) ) {
1066 return array(
1067 'flow_id' => $flow_id,
1068 'flow_title' => esc_html( get_the_title( $flow_id ) ),
1069 'url_edit' => esc_url( admin_url( 'admin.php?page=cartflows&path=flows&action=wcf-edit-flow&flow_id=' . $flow_id ) ),
1070 'is_configured' => true,
1071 );
1072 }
1073
1074 return array(
1075 'flow_id' => 0,
1076 'flow_title' => '',
1077 'url_edit' => '',
1078 'is_configured' => false,
1079 );
1080
1081 } catch ( \Exception $e ) {
1082 return $this->error( $e );
1083 }
1084 }
1085
1086 /**
1087 * Return CartFlows permalink (URL slug) settings.
1088 *
1089 * @param array $input Validated input.
1090 * @return array
1091 * @throws \Exception On failure.
1092 */
1093 public function get_permalink_settings( $input ) {
1094
1095 try {
1096 $this->init( $input, CARTFLOWS_ABILITY_API_NAMESPACE . 'get-permalink-settings' );
1097
1098 $settings = \Cartflows_Helper::get_permalink_settings();
1099
1100 return array(
1101 'permalink' => esc_html( isset( $settings['permalink'] ) ? $settings['permalink'] : '' ),
1102 'permalink_flow_base' => esc_html( isset( $settings['permalink_flow_base'] ) ? $settings['permalink_flow_base'] : '' ),
1103 'permalink_structure' => esc_html( isset( $settings['permalink_structure'] ) ? $settings['permalink_structure'] : '' ),
1104 );
1105
1106 } catch ( \Exception $e ) {
1107 return $this->error( $e );
1108 }
1109 }
1110
1111 /**
1112 * Return pixel / analytics integration settings, optionally filtered to one group.
1113 *
1114 * @param array $input Validated input.
1115 * @return array
1116 * @throws \Exception On failure.
1117 */
1118 public function get_integration_settings( $input ) {
1119
1120 try {
1121 $this->init( $input, CARTFLOWS_ABILITY_API_NAMESPACE . 'get-integration-settings' );
1122
1123 $integration = $this->input_get( 'integration' );
1124
1125 $map = array(
1126 'facebook' => '_cartflows_facebook',
1127 'google_analytics' => '_cartflows_google_analytics',
1128 'google_ads' => '_cartflows_google_ads',
1129 'tiktok' => '_cartflows_tiktok',
1130 'pinterest' => '_cartflows_pinterest',
1131 'snapchat' => '_cartflows_snapchat',
1132 );
1133
1134 $result = array();
1135
1136 if ( 'all' === $integration ) {
1137 foreach ( $map as $key => $option_name ) {
1138 $value = \Cartflows_Helper::get_admin_settings_option( $option_name, array() );
1139 $result[ $key ] = is_array( $value ) ? $value : array();
1140 }
1141 } else {
1142 if ( ! isset( $map[ $integration ] ) ) {
1143 throw new \Exception( esc_html__( 'Invalid integration group.', 'cartflows' ) );
1144 }
1145 $value = \Cartflows_Helper::get_admin_settings_option( $map[ $integration ], array() );
1146 $result[ $integration ] = is_array( $value ) ? $value : array();
1147 }
1148
1149 return $result;
1150
1151 } catch ( \Exception $e ) {
1152 return $this->error( $e );
1153 }
1154 }
1155
1156 // ============================================================
1157 // Execute Callbacks — Admin / Settings Write
1158 // ============================================================
1159
1160 /**
1161 * Update CartFlows general settings.
1162 *
1163 * Only fields present in $input (non-default) are merged in; omitted fields keep existing values.
1164 *
1165 * @param array $input Validated input.
1166 * @return array
1167 * @throws \Exception On failure.
1168 */
1169 public function update_general_settings( $input ) {
1170
1171 try {
1172 $this->init( $input, CARTFLOWS_ABILITY_API_NAMESPACE . 'update-general-settings' );
1173
1174 $current = \Cartflows_Helper::get_common_settings();
1175
1176 $allowed_keys = array( 'default_page_builder', 'global_checkout', 'override_global_checkout', 'disallow_indexing' );
1177
1178 $updates = array();
1179 foreach ( $allowed_keys as $key ) {
1180 $value = $this->input_get( $key, null );
1181 if ( null !== $value && '' !== $value ) {
1182 $updates[ $key ] = sanitize_text_field( $value );
1183 }
1184 }
1185
1186 if ( empty( $updates ) ) {
1187 throw new \Exception( esc_html__( 'No settings provided to update.', 'cartflows' ) );
1188 }
1189
1190 $new_settings = wp_parse_args( $updates, $current );
1191
1192 \Cartflows_Helper::update_admin_settings_option( '_cartflows_common', $new_settings, false );
1193
1194 return array(
1195 'message' => esc_html__( 'General settings updated successfully.', 'cartflows' ),
1196 'settings' => array(
1197 'default_page_builder' => esc_html( isset( $new_settings['default_page_builder'] ) ? $new_settings['default_page_builder'] : '' ),
1198 'global_checkout' => esc_html( isset( $new_settings['global_checkout'] ) ? $new_settings['global_checkout'] : '' ),
1199 'override_global_checkout' => esc_html( isset( $new_settings['override_global_checkout'] ) ? $new_settings['override_global_checkout'] : '' ),
1200 'override_store_order_pay' => esc_html( isset( $new_settings['override_store_order_pay'] ) ? $new_settings['override_store_order_pay'] : '' ),
1201 'disallow_indexing' => esc_html( isset( $new_settings['disallow_indexing'] ) ? $new_settings['disallow_indexing'] : '' ),
1202 ),
1203 );
1204
1205 } catch ( \Exception $e ) {
1206 return $this->error( $e );
1207 }
1208 }
1209
1210 /**
1211 * Update CartFlows permalink (URL slug) settings.
1212 *
1213 * Forces a permalink flush after saving.
1214 *
1215 * @param array $input Validated input.
1216 * @return array
1217 * @throws \Exception On failure.
1218 */
1219 public function update_permalink_settings( $input ) {
1220
1221 try {
1222 $this->init( $input, CARTFLOWS_ABILITY_API_NAMESPACE . 'update-permalink-settings' );
1223
1224 $current = \Cartflows_Helper::get_permalink_settings();
1225
1226 $permalink = $this->input_get( 'permalink', null );
1227 $permalink_flow_base = $this->input_get( 'permalink_flow_base', null );
1228 $permalink_structure = $this->input_get( 'permalink_structure', null );
1229
1230 if ( null === $permalink && null === $permalink_flow_base && null === $permalink_structure ) {
1231 throw new \Exception( esc_html__( 'No permalink settings provided to update.', 'cartflows' ) );
1232 }
1233
1234 $new_settings = $current;
1235
1236 if ( null !== $permalink ) {
1237 $new_settings['permalink'] = ! empty( $permalink )
1238 ? sanitize_title( $permalink )
1239 : CARTFLOWS_STEP_PERMALINK_SLUG;
1240 }
1241
1242 if ( null !== $permalink_flow_base ) {
1243 $new_settings['permalink_flow_base'] = ! empty( $permalink_flow_base )
1244 ? sanitize_title( $permalink_flow_base )
1245 : CARTFLOWS_FLOW_PERMALINK_SLUG;
1246 }
1247
1248 if ( null !== $permalink_structure ) {
1249 $new_settings['permalink_structure'] = sanitize_text_field( $permalink_structure );
1250 }
1251
1252 \Cartflows_Helper::update_admin_settings_option( '_cartflows_permalink', $new_settings, true );
1253 update_option( 'cartflows_permalink_refresh', true );
1254
1255 return array(
1256 'message' => esc_html__( 'Permalink settings updated successfully.', 'cartflows' ),
1257 'permalink' => esc_html( $new_settings['permalink'] ),
1258 'permalink_flow_base' => esc_html( $new_settings['permalink_flow_base'] ),
1259 );
1260
1261 } catch ( \Exception $e ) {
1262 return $this->error( $e );
1263 }
1264 }
1265
1266 // ============================================================
1267 // Execute Callbacks — Checkout Read
1268 // ============================================================
1269
1270 /**
1271 * Get checkout step settings: layout, products, form toggles, button, and design options.
1272 *
1273 * @param array $input Validated input.
1274 * @return array
1275 * @throws \Exception On failure.
1276 */
1277 public function get_checkout_settings( $input ) {
1278
1279 try {
1280 $this->init( $input, CARTFLOWS_ABILITY_API_NAMESPACE . 'get-checkout-settings' );
1281
1282 if ( ! function_exists( 'WC' ) ) {
1283 throw new \Exception( esc_html__( 'WooCommerce is required for checkout abilities.', 'cartflows' ) );
1284 }
1285
1286 $step_id = $this->input_get( 'step_id' );
1287
1288 if ( null === $step_id || 0 === intval( $step_id ) ) {
1289 throw new \Exception( esc_html__( 'Invalid step ID.', 'cartflows' ) );
1290 }
1291
1292 if ( get_post_type( $step_id ) !== CARTFLOWS_STEP_POST_TYPE ) {
1293 throw new \Exception( esc_html__( 'Step not found.', 'cartflows' ) );
1294 }
1295
1296 $step_type = get_post_meta( $step_id, 'wcf-step-type', true );
1297 if ( 'checkout' !== $step_type ) {
1298 throw new \Exception( esc_html__( 'Step is not a checkout type.', 'cartflows' ) );
1299 }
1300
1301 // Layout.
1302 $layout = wcf()->options->get_checkout_meta_value( $step_id, 'wcf-checkout-layout' );
1303
1304 // Primary color and font.
1305 $primary_color = wcf()->options->get_checkout_meta_value( $step_id, 'wcf-primary-color' );
1306 $font_family = wcf()->options->get_checkout_meta_value( $step_id, 'wcf-base-font-family' );
1307
1308 // Products.
1309 $products_raw = wcf()->options->get_checkout_meta_value( $step_id, 'wcf-checkout-products' );
1310 $products = array();
1311
1312 if ( is_array( $products_raw ) && ! empty( $products_raw ) ) {
1313 foreach ( $products_raw as $product ) {
1314 $product_id = isset( $product['product'] ) ? intval( $product['product'] ) : 0;
1315 $product_obj = ( $product_id > 0 ) ? wc_get_product( $product_id ) : null;
1316
1317 $products[] = array(
1318 'product_id' => $product_id,
1319 'name' => $product_obj ? esc_html( $product_obj->get_name() ) : '',
1320 'quantity' => isset( $product['quantity'] ) ? intval( $product['quantity'] ) : 1,
1321 'discount_type' => isset( $product['discount_type'] ) ? esc_html( $product['discount_type'] ) : '',
1322 'discount_value' => isset( $product['discount_value'] ) ? esc_html( $product['discount_value'] ) : '',
1323 'add_to_cart' => isset( $product['add_to_cart'] ) ? esc_html( $product['add_to_cart'] ) : 'yes',
1324 );
1325 }
1326 }
1327
1328 // Form settings.
1329 $form_settings = array(
1330 'show_coupon_field' => esc_html( wcf()->options->get_checkout_meta_value( $step_id, 'wcf-show-coupon-field' ) ),
1331 'optimize_coupon_field' => esc_html( wcf()->options->get_checkout_meta_value( $step_id, 'wcf-optimize-coupon-field' ) ),
1332 'show_additional_fields' => esc_html( wcf()->options->get_checkout_meta_value( $step_id, 'wcf-checkout-additional-fields' ) ),
1333 'optimize_order_note' => esc_html( wcf()->options->get_checkout_meta_value( $step_id, 'wcf-optimize-order-note-field' ) ),
1334 'ship_to_different_address' => esc_html( wcf()->options->get_checkout_meta_value( $step_id, 'wcf-shipto-diff-addr-fields' ) ),
1335 'google_autoaddress' => esc_html( wcf()->options->get_checkout_meta_value( $step_id, 'wcf-google-autoaddress' ) ),
1336 );
1337
1338 // Button settings.
1339 $button_settings = array(
1340 'button_text' => esc_html( wcf()->options->get_checkout_meta_value( $step_id, 'wcf-checkout-place-order-button-text' ) ),
1341 'show_lock_icon' => esc_html( wcf()->options->get_checkout_meta_value( $step_id, 'wcf-checkout-place-order-button-lock' ) ),
1342 'show_price' => esc_html( wcf()->options->get_checkout_meta_value( $step_id, 'wcf-checkout-place-order-button-price-display' ) ),
1343 );
1344
1345 // Advanced settings.
1346 $advanced_settings = array(
1347 'show_product_images' => esc_html( wcf()->options->get_checkout_meta_value( $step_id, 'wcf-order-review-show-product-images' ) ),
1348 'enable_cart_editing' => esc_html( wcf()->options->get_checkout_meta_value( $step_id, 'wcf-remove-product-field' ) ),
1349 );
1350
1351 return array(
1352 'step_id' => intval( $step_id ),
1353 'layout' => esc_html( $layout ),
1354 'primary_color' => esc_html( $primary_color ),
1355 'font_family' => esc_html( $font_family ),
1356 'products' => $products,
1357 'form_settings' => $form_settings,
1358 'button_settings' => $button_settings,
1359 'advanced_settings' => $advanced_settings,
1360 );
1361
1362 } catch ( \Exception $e ) {
1363 return $this->error( $e );
1364 }
1365 }
1366
1367 /**
1368 * Get products assigned to a checkout step.
1369 *
1370 * @param array $input Validated input.
1371 * @return array
1372 * @throws \Exception On failure.
1373 */
1374 public function get_checkout_products( $input ) {
1375
1376 try {
1377 $this->init( $input, CARTFLOWS_ABILITY_API_NAMESPACE . 'get-checkout-products' );
1378
1379 if ( ! function_exists( 'WC' ) ) {
1380 throw new \Exception( esc_html__( 'WooCommerce is required for checkout abilities.', 'cartflows' ) );
1381 }
1382
1383 $step_id = $this->input_get( 'step_id' );
1384
1385 if ( null === $step_id || 0 === intval( $step_id ) ) {
1386 throw new \Exception( esc_html__( 'Invalid step ID.', 'cartflows' ) );
1387 }
1388
1389 if ( get_post_type( $step_id ) !== CARTFLOWS_STEP_POST_TYPE ) {
1390 throw new \Exception( esc_html__( 'Step not found.', 'cartflows' ) );
1391 }
1392
1393 $step_type = get_post_meta( $step_id, 'wcf-step-type', true );
1394 if ( 'checkout' !== $step_type ) {
1395 throw new \Exception( esc_html__( 'Step is not a checkout type.', 'cartflows' ) );
1396 }
1397
1398 $products_raw = wcf()->options->get_checkout_meta_value( $step_id, 'wcf-checkout-products' );
1399 $products = array();
1400
1401 if ( is_array( $products_raw ) && ! empty( $products_raw ) ) {
1402 foreach ( $products_raw as $product ) {
1403 $product_id = isset( $product['product'] ) ? intval( $product['product'] ) : 0;
1404 $product_obj = ( $product_id > 0 ) ? wc_get_product( $product_id ) : null;
1405
1406 $products[] = array(
1407 'product_id' => $product_id,
1408 'name' => $product_obj ? esc_html( $product_obj->get_name() ) : '',
1409 'quantity' => isset( $product['quantity'] ) ? intval( $product['quantity'] ) : 1,
1410 'discount_type' => isset( $product['discount_type'] ) ? esc_html( $product['discount_type'] ) : '',
1411 'discount_value' => isset( $product['discount_value'] ) ? esc_html( $product['discount_value'] ) : '',
1412 'add_to_cart' => isset( $product['add_to_cart'] ) ? esc_html( $product['add_to_cart'] ) : 'yes',
1413 'img_url' => $product_obj ? esc_url( (string) get_the_post_thumbnail_url( $product_id ) ) : '',
1414 'regular_price' => $product_obj ? esc_html( \Cartflows_Helper::get_product_original_price( $product_obj ) ) : '',
1415 );
1416 }
1417 }
1418
1419 return array(
1420 'step_id' => intval( $step_id ),
1421 'products' => $products,
1422 );
1423
1424 } catch ( \Exception $e ) {
1425 return $this->error( $e );
1426 }
1427 }
1428
1429 /**
1430 * Get checkout form field configuration.
1431 *
1432 * @param array $input Validated input.
1433 * @return array
1434 * @throws \Exception On failure.
1435 */
1436 public function get_checkout_fields( $input ) {
1437
1438 try {
1439 $this->init( $input, CARTFLOWS_ABILITY_API_NAMESPACE . 'get-checkout-fields' );
1440
1441 if ( ! function_exists( 'WC' ) ) {
1442 throw new \Exception( esc_html__( 'WooCommerce is required for checkout abilities.', 'cartflows' ) );
1443 }
1444
1445 $step_id = $this->input_get( 'step_id' );
1446 $field_type = $this->input_get( 'field_type' );
1447
1448 if ( null === $step_id || 0 === intval( $step_id ) ) {
1449 throw new \Exception( esc_html__( 'Invalid step ID.', 'cartflows' ) );
1450 }
1451
1452 if ( get_post_type( $step_id ) !== CARTFLOWS_STEP_POST_TYPE ) {
1453 throw new \Exception( esc_html__( 'Step not found.', 'cartflows' ) );
1454 }
1455
1456 $step_type = get_post_meta( $step_id, 'wcf-step-type', true );
1457 if ( 'checkout' !== $step_type ) {
1458 throw new \Exception( esc_html__( 'Step is not a checkout type.', 'cartflows' ) );
1459 }
1460
1461 $billing_fields = array();
1462 $shipping_fields = array();
1463
1464 if ( 'all' === $field_type || 'billing' === $field_type ) {
1465 $billing_raw = wcf()->options->get_checkout_meta_value( $step_id, 'wcf_field_order_billing' );
1466 if ( ! is_array( $billing_raw ) || empty( $billing_raw ) ) {
1467 $billing_raw = \Cartflows_Helper::get_checkout_fields( 'billing', $step_id );
1468 }
1469 $billing_fields = is_array( $billing_raw ) ? $billing_raw : array();
1470 }
1471
1472 if ( 'all' === $field_type || 'shipping' === $field_type ) {
1473 $shipping_raw = wcf()->options->get_checkout_meta_value( $step_id, 'wcf_field_order_shipping' );
1474 if ( ! is_array( $shipping_raw ) || empty( $shipping_raw ) ) {
1475 $shipping_raw = \Cartflows_Helper::get_checkout_fields( 'shipping', $step_id );
1476 }
1477 $shipping_fields = is_array( $shipping_raw ) ? $shipping_raw : array();
1478 }
1479
1480 $form_settings = array(
1481 'show_coupon_field' => esc_html( wcf()->options->get_checkout_meta_value( $step_id, 'wcf-show-coupon-field' ) ),
1482 'optimize_coupon_field' => esc_html( wcf()->options->get_checkout_meta_value( $step_id, 'wcf-optimize-coupon-field' ) ),
1483 'show_additional_fields' => esc_html( wcf()->options->get_checkout_meta_value( $step_id, 'wcf-checkout-additional-fields' ) ),
1484 'optimize_order_note' => esc_html( wcf()->options->get_checkout_meta_value( $step_id, 'wcf-optimize-order-note-field' ) ),
1485 'ship_to_different_address' => esc_html( wcf()->options->get_checkout_meta_value( $step_id, 'wcf-shipto-diff-addr-fields' ) ),
1486 'google_autoaddress' => esc_html( wcf()->options->get_checkout_meta_value( $step_id, 'wcf-google-autoaddress' ) ),
1487 );
1488
1489 return array(
1490 'step_id' => intval( $step_id ),
1491 'billing_fields' => $billing_fields,
1492 'shipping_fields' => $shipping_fields,
1493 'form_settings' => $form_settings,
1494 );
1495
1496 } catch ( \Exception $e ) {
1497 return $this->error( $e );
1498 }
1499 }
1500
1501 // ============================================================
1502 // Execute Callbacks — Checkout Write
1503 // ============================================================
1504
1505 /**
1506 * Update products assigned to a checkout step.
1507 *
1508 * @param array $input Validated input.
1509 * @return array
1510 * @throws \Exception On failure.
1511 */
1512 public function update_checkout_products( $input ) {
1513
1514 try {
1515 $this->init( $input, CARTFLOWS_ABILITY_API_NAMESPACE . 'update-checkout-products' );
1516
1517 if ( ! function_exists( 'WC' ) ) {
1518 throw new \Exception( esc_html__( 'WooCommerce is required for checkout abilities.', 'cartflows' ) );
1519 }
1520
1521 $step_id = $this->input_get( 'step_id' );
1522 $products = $this->input_get( 'products' );
1523
1524 if ( null === $step_id || 0 === intval( $step_id ) ) {
1525 throw new \Exception( esc_html__( 'Invalid step ID.', 'cartflows' ) );
1526 }
1527
1528 if ( get_post_type( $step_id ) !== CARTFLOWS_STEP_POST_TYPE ) {
1529 throw new \Exception( esc_html__( 'Step not found.', 'cartflows' ) );
1530 }
1531
1532 $step_type = get_post_meta( $step_id, 'wcf-step-type', true );
1533 if ( 'checkout' !== $step_type ) {
1534 throw new \Exception( esc_html__( 'Step is not a checkout type.', 'cartflows' ) );
1535 }
1536
1537 if ( ! is_array( $products ) ) {
1538 throw new \Exception( esc_html__( 'Products must be an array.', 'cartflows' ) );
1539 }
1540
1541 $sanitized_products = array();
1542
1543 foreach ( $products as $index => $product ) {
1544 $product_id = isset( $product['product'] ) ? intval( $product['product'] ) : 0;
1545
1546 if ( $product_id <= 0 ) {
1547 continue;
1548 }
1549
1550 $product_obj = wc_get_product( $product_id );
1551 if ( ! $product_obj ) {
1552 throw new \Exception(
1553 sprintf(
1554 /* translators: %d: product ID */
1555 esc_html__( 'WooCommerce product %d not found.', 'cartflows' ),
1556 $product_id
1557 )
1558 );
1559 }
1560
1561 $unique_id = isset( $product['unique_id'] ) ? sanitize_text_field( $product['unique_id'] ) : md5( $product_id . '_' . $index . '_' . time() );
1562
1563 $sanitized_products[] = array(
1564 'product' => $product_id,
1565 'quantity' => isset( $product['quantity'] ) ? max( 1, intval( $product['quantity'] ) ) : 1,
1566 'discount_type' => isset( $product['discount_type'] ) ? sanitize_text_field( $product['discount_type'] ) : '',
1567 'discount_value' => isset( $product['discount_value'] ) ? sanitize_text_field( $product['discount_value'] ) : '',
1568 'add_to_cart' => isset( $product['add_to_cart'] ) ? sanitize_text_field( $product['add_to_cart'] ) : 'yes',
1569 'unique_id' => $unique_id,
1570 );
1571 }
1572
1573 update_post_meta( $step_id, 'wcf-checkout-products', $sanitized_products );
1574
1575 return array(
1576 'step_id' => intval( $step_id ),
1577 'products' => $sanitized_products,
1578 'message' => esc_html__( 'Checkout products updated successfully.', 'cartflows' ),
1579 );
1580
1581 } catch ( \Exception $e ) {
1582 return $this->error( $e );
1583 }
1584 }
1585
1586 /**
1587 * Update checkout layout/skin.
1588 *
1589 * @param array $input Validated input.
1590 * @return array
1591 * @throws \Exception On failure.
1592 */
1593 public function update_checkout_layout( $input ) {
1594
1595 try {
1596 $this->init( $input, CARTFLOWS_ABILITY_API_NAMESPACE . 'update-checkout-layout' );
1597
1598 if ( ! function_exists( 'WC' ) ) {
1599 throw new \Exception( esc_html__( 'WooCommerce is required for checkout abilities.', 'cartflows' ) );
1600 }
1601
1602 $step_id = $this->input_get( 'step_id' );
1603 $layout = $this->input_get( 'layout' );
1604
1605 if ( null === $step_id || 0 === intval( $step_id ) ) {
1606 throw new \Exception( esc_html__( 'Invalid step ID.', 'cartflows' ) );
1607 }
1608
1609 if ( get_post_type( $step_id ) !== CARTFLOWS_STEP_POST_TYPE ) {
1610 throw new \Exception( esc_html__( 'Step not found.', 'cartflows' ) );
1611 }
1612
1613 $step_type = get_post_meta( $step_id, 'wcf-step-type', true );
1614 if ( 'checkout' !== $step_type ) {
1615 throw new \Exception( esc_html__( 'Step is not a checkout type.', 'cartflows' ) );
1616 }
1617
1618 update_post_meta( $step_id, 'wcf-checkout-layout', sanitize_text_field( $layout ) );
1619
1620 return array(
1621 'step_id' => intval( $step_id ),
1622 'layout' => esc_html( $layout ),
1623 'message' => esc_html__( 'Checkout layout updated successfully.', 'cartflows' ),
1624 );
1625
1626 } catch ( \Exception $e ) {
1627 return $this->error( $e );
1628 }
1629 }
1630
1631 /**
1632 * Update place order button settings.
1633 *
1634 * @param array $input Validated input.
1635 * @return array
1636 * @throws \Exception On failure.
1637 */
1638 public function update_checkout_place_order_button( $input ) {
1639
1640 try {
1641 $this->init( $input, CARTFLOWS_ABILITY_API_NAMESPACE . 'update-checkout-place-order-button' );
1642
1643 if ( ! function_exists( 'WC' ) ) {
1644 throw new \Exception( esc_html__( 'WooCommerce is required for checkout abilities.', 'cartflows' ) );
1645 }
1646
1647 $step_id = $this->input_get( 'step_id' );
1648
1649 if ( null === $step_id || 0 === intval( $step_id ) ) {
1650 throw new \Exception( esc_html__( 'Invalid step ID.', 'cartflows' ) );
1651 }
1652
1653 if ( get_post_type( $step_id ) !== CARTFLOWS_STEP_POST_TYPE ) {
1654 throw new \Exception( esc_html__( 'Step not found.', 'cartflows' ) );
1655 }
1656
1657 $step_type = get_post_meta( $step_id, 'wcf-step-type', true );
1658 if ( 'checkout' !== $step_type ) {
1659 throw new \Exception( esc_html__( 'Step is not a checkout type.', 'cartflows' ) );
1660 }
1661
1662 $button_text = $this->input_get( 'button_text', null );
1663 $show_lock_icon = $this->input_get( 'show_lock_icon', null );
1664 $show_price = $this->input_get( 'show_price', null );
1665
1666 $updated = false;
1667
1668 if ( null !== $button_text && '' !== $button_text ) {
1669 update_post_meta( $step_id, 'wcf-checkout-place-order-button-text', sanitize_text_field( $button_text ) );
1670 $updated = true;
1671 }
1672
1673 if ( null !== $show_lock_icon && '' !== $show_lock_icon ) {
1674 update_post_meta( $step_id, 'wcf-checkout-place-order-button-lock', sanitize_text_field( $show_lock_icon ) );
1675 $updated = true;
1676 }
1677
1678 if ( null !== $show_price && '' !== $show_price ) {
1679 update_post_meta( $step_id, 'wcf-checkout-place-order-button-price-display', sanitize_text_field( $show_price ) );
1680 $updated = true;
1681 }
1682
1683 if ( ! $updated ) {
1684 throw new \Exception( esc_html__( 'No button settings provided to update.', 'cartflows' ) );
1685 }
1686
1687 return array(
1688 'step_id' => intval( $step_id ),
1689 'button_text' => esc_html( wcf()->options->get_checkout_meta_value( $step_id, 'wcf-checkout-place-order-button-text' ) ),
1690 'show_lock_icon' => esc_html( wcf()->options->get_checkout_meta_value( $step_id, 'wcf-checkout-place-order-button-lock' ) ),
1691 'show_price' => esc_html( wcf()->options->get_checkout_meta_value( $step_id, 'wcf-checkout-place-order-button-price-display' ) ),
1692 'message' => esc_html__( 'Place order button updated successfully.', 'cartflows' ),
1693 );
1694
1695 } catch ( \Exception $e ) {
1696 return $this->error( $e );
1697 }
1698 }
1699
1700 /**
1701 * Update checkout form-level settings (toggles).
1702 *
1703 * @param array $input Validated input.
1704 * @return array
1705 * @throws \Exception On failure.
1706 */
1707 public function update_checkout_form_settings( $input ) {
1708
1709 try {
1710 $this->init( $input, CARTFLOWS_ABILITY_API_NAMESPACE . 'update-checkout-form-settings' );
1711
1712 if ( ! function_exists( 'WC' ) ) {
1713 throw new \Exception( esc_html__( 'WooCommerce is required for checkout abilities.', 'cartflows' ) );
1714 }
1715
1716 $step_id = $this->input_get( 'step_id' );
1717
1718 if ( null === $step_id || 0 === intval( $step_id ) ) {
1719 throw new \Exception( esc_html__( 'Invalid step ID.', 'cartflows' ) );
1720 }
1721
1722 if ( get_post_type( $step_id ) !== CARTFLOWS_STEP_POST_TYPE ) {
1723 throw new \Exception( esc_html__( 'Step not found.', 'cartflows' ) );
1724 }
1725
1726 $step_type = get_post_meta( $step_id, 'wcf-step-type', true );
1727 if ( 'checkout' !== $step_type ) {
1728 throw new \Exception( esc_html__( 'Step is not a checkout type.', 'cartflows' ) );
1729 }
1730
1731 // Map input field names to post meta keys.
1732 $field_meta_map = array(
1733 'show_coupon_field' => 'wcf-show-coupon-field',
1734 'optimize_coupon_field' => 'wcf-optimize-coupon-field',
1735 'show_additional_fields' => 'wcf-checkout-additional-fields',
1736 'optimize_order_note' => 'wcf-optimize-order-note-field',
1737 'ship_to_different_address' => 'wcf-shipto-diff-addr-fields',
1738 'google_autoaddress' => 'wcf-google-autoaddress',
1739 'show_product_images' => 'wcf-order-review-show-product-images',
1740 'enable_cart_editing' => 'wcf-remove-product-field',
1741 );
1742
1743 $updated = false;
1744 $settings = array();
1745
1746 foreach ( $field_meta_map as $input_key => $meta_key ) {
1747 $value = $this->input_get( $input_key, null );
1748
1749 if ( null !== $value && '' !== $value ) {
1750 update_post_meta( $step_id, $meta_key, sanitize_text_field( $value ) );
1751 $updated = true;
1752 $settings[ $input_key ] = esc_html( $value );
1753 } else {
1754 $settings[ $input_key ] = esc_html( wcf()->options->get_checkout_meta_value( $step_id, $meta_key ) );
1755 }
1756 }
1757
1758 if ( ! $updated ) {
1759 throw new \Exception( esc_html__( 'No form settings provided to update.', 'cartflows' ) );
1760 }
1761
1762 return array(
1763 'step_id' => intval( $step_id ),
1764 'settings' => $settings,
1765 'message' => esc_html__( 'Checkout form settings updated successfully.', 'cartflows' ),
1766 );
1767
1768 } catch ( \Exception $e ) {
1769 return $this->error( $e );
1770 }
1771 }
1772
1773 // ============================================================
1774 // Execute Callbacks — Thank You Read
1775 // ============================================================
1776
1777 /**
1778 * Get thank you step settings: layout, section visibility, custom text, redirect, and design.
1779 *
1780 * @param array $input Validated input.
1781 * @return array
1782 * @throws \Exception On failure.
1783 */
1784 public function get_thankyou_settings( $input ) {
1785
1786 try {
1787 $this->init( $input, CARTFLOWS_ABILITY_API_NAMESPACE . 'get-thankyou-settings' );
1788
1789 if ( ! function_exists( 'WC' ) ) {
1790 throw new \Exception( esc_html__( 'WooCommerce is required for thank you abilities.', 'cartflows' ) );
1791 }
1792
1793 $step_id = $this->input_get( 'step_id' );
1794
1795 if ( null === $step_id || 0 === intval( $step_id ) ) {
1796 throw new \Exception( esc_html__( 'Invalid step ID.', 'cartflows' ) );
1797 }
1798
1799 if ( get_post_type( $step_id ) !== CARTFLOWS_STEP_POST_TYPE ) {
1800 throw new \Exception( esc_html__( 'Step not found.', 'cartflows' ) );
1801 }
1802
1803 $step_type = get_post_meta( $step_id, 'wcf-step-type', true );
1804 if ( 'thankyou' !== $step_type ) {
1805 throw new \Exception( esc_html__( 'Step is not a thank you type.', 'cartflows' ) );
1806 }
1807
1808 // Layout.
1809 $layout = wcf()->options->get_thankyou_meta_value( $step_id, 'wcf-tq-layout' );
1810
1811 // Section visibility.
1812 $sections = array(
1813 'show_overview' => esc_html( wcf()->options->get_thankyou_meta_value( $step_id, 'wcf-show-overview-section' ) ),
1814 'show_details' => esc_html( wcf()->options->get_thankyou_meta_value( $step_id, 'wcf-show-details-section' ) ),
1815 'show_billing' => esc_html( wcf()->options->get_thankyou_meta_value( $step_id, 'wcf-show-billing-section' ) ),
1816 'show_shipping' => esc_html( wcf()->options->get_thankyou_meta_value( $step_id, 'wcf-show-shipping-section' ) ),
1817 );
1818
1819 // Custom text.
1820 $custom_text = wcf()->options->get_thankyou_meta_value( $step_id, 'wcf-tq-text' );
1821
1822 // Redirect settings.
1823 $redirect = array(
1824 'enabled' => esc_html( wcf()->options->get_thankyou_meta_value( $step_id, 'wcf-show-tq-redirect-section' ) ),
1825 'redirect_url' => esc_url( wcf()->options->get_thankyou_meta_value( $step_id, 'wcf-tq-redirect-link' ) ),
1826 );
1827
1828 // Design settings.
1829 $design = array(
1830 'primary_color' => esc_html( wcf()->options->get_thankyou_meta_value( $step_id, 'wcf-tq-primary-color' ) ),
1831 'text_color' => esc_html( wcf()->options->get_thankyou_meta_value( $step_id, 'wcf-tq-text-color' ) ),
1832 'text_font_family' => esc_html( wcf()->options->get_thankyou_meta_value( $step_id, 'wcf-tq-font-family' ) ),
1833 'text_font_size' => esc_html( wcf()->options->get_thankyou_meta_value( $step_id, 'wcf-tq-font-size' ) ),
1834 'heading_color' => esc_html( wcf()->options->get_thankyou_meta_value( $step_id, 'wcf-tq-heading-color' ) ),
1835 'heading_font_family' => esc_html( wcf()->options->get_thankyou_meta_value( $step_id, 'wcf-tq-heading-font-family' ) ),
1836 'heading_font_weight' => esc_html( wcf()->options->get_thankyou_meta_value( $step_id, 'wcf-tq-heading-font-wt' ) ),
1837 'container_width' => esc_html( wcf()->options->get_thankyou_meta_value( $step_id, 'wcf-tq-container-width' ) ),
1838 'section_bg_color' => esc_html( wcf()->options->get_thankyou_meta_value( $step_id, 'wcf-tq-section-bg-color' ) ),
1839 );
1840
1841 return array(
1842 'step_id' => intval( $step_id ),
1843 'layout' => esc_html( $layout ),
1844 'sections' => $sections,
1845 'custom_text' => esc_html( $custom_text ),
1846 'redirect' => $redirect,
1847 'design' => $design,
1848 );
1849
1850 } catch ( \Exception $e ) {
1851 return $this->error( $e );
1852 }
1853 }
1854
1855 // ============================================================
1856 // Execute Callbacks — Thank You Write
1857 // ============================================================
1858
1859 /**
1860 * Update thank you layout/skin.
1861 *
1862 * @param array $input Validated input.
1863 * @return array
1864 * @throws \Exception On failure.
1865 */
1866 public function update_thankyou_layout( $input ) {
1867
1868 try {
1869 $this->init( $input, CARTFLOWS_ABILITY_API_NAMESPACE . 'update-thankyou-layout' );
1870
1871 if ( ! function_exists( 'WC' ) ) {
1872 throw new \Exception( esc_html__( 'WooCommerce is required for thank you abilities.', 'cartflows' ) );
1873 }
1874
1875 $step_id = $this->input_get( 'step_id' );
1876 $layout = $this->input_get( 'layout' );
1877
1878 if ( null === $step_id || 0 === intval( $step_id ) ) {
1879 throw new \Exception( esc_html__( 'Invalid step ID.', 'cartflows' ) );
1880 }
1881
1882 if ( get_post_type( $step_id ) !== CARTFLOWS_STEP_POST_TYPE ) {
1883 throw new \Exception( esc_html__( 'Step not found.', 'cartflows' ) );
1884 }
1885
1886 $step_type = get_post_meta( $step_id, 'wcf-step-type', true );
1887 if ( 'thankyou' !== $step_type ) {
1888 throw new \Exception( esc_html__( 'Step is not a thank you type.', 'cartflows' ) );
1889 }
1890
1891 update_post_meta( $step_id, 'wcf-tq-layout', sanitize_text_field( $layout ) );
1892
1893 return array(
1894 'step_id' => intval( $step_id ),
1895 'layout' => esc_html( $layout ),
1896 'message' => esc_html__( 'Thank you layout updated successfully.', 'cartflows' ),
1897 );
1898
1899 } catch ( \Exception $e ) {
1900 return $this->error( $e );
1901 }
1902 }
1903
1904 /**
1905 * Update thank you section visibility toggles.
1906 *
1907 * @param array $input Validated input.
1908 * @return array
1909 * @throws \Exception On failure.
1910 */
1911 public function update_thankyou_sections( $input ) {
1912
1913 try {
1914 $this->init( $input, CARTFLOWS_ABILITY_API_NAMESPACE . 'update-thankyou-sections' );
1915
1916 if ( ! function_exists( 'WC' ) ) {
1917 throw new \Exception( esc_html__( 'WooCommerce is required for thank you abilities.', 'cartflows' ) );
1918 }
1919
1920 $step_id = $this->input_get( 'step_id' );
1921
1922 if ( null === $step_id || 0 === intval( $step_id ) ) {
1923 throw new \Exception( esc_html__( 'Invalid step ID.', 'cartflows' ) );
1924 }
1925
1926 if ( get_post_type( $step_id ) !== CARTFLOWS_STEP_POST_TYPE ) {
1927 throw new \Exception( esc_html__( 'Step not found.', 'cartflows' ) );
1928 }
1929
1930 $step_type = get_post_meta( $step_id, 'wcf-step-type', true );
1931 if ( 'thankyou' !== $step_type ) {
1932 throw new \Exception( esc_html__( 'Step is not a thank you type.', 'cartflows' ) );
1933 }
1934
1935 $field_meta_map = array(
1936 'show_overview' => 'wcf-show-overview-section',
1937 'show_details' => 'wcf-show-details-section',
1938 'show_billing' => 'wcf-show-billing-section',
1939 'show_shipping' => 'wcf-show-shipping-section',
1940 );
1941
1942 $updated = false;
1943 $sections = array();
1944
1945 foreach ( $field_meta_map as $input_key => $meta_key ) {
1946 $value = $this->input_get( $input_key, null );
1947
1948 if ( null !== $value && '' !== $value ) {
1949 update_post_meta( $step_id, $meta_key, sanitize_text_field( $value ) );
1950 $updated = true;
1951 $sections[ $input_key ] = esc_html( $value );
1952 } else {
1953 $sections[ $input_key ] = esc_html( wcf()->options->get_thankyou_meta_value( $step_id, $meta_key ) );
1954 }
1955 }
1956
1957 if ( ! $updated ) {
1958 throw new \Exception( esc_html__( 'No section settings provided to update.', 'cartflows' ) );
1959 }
1960
1961 return array(
1962 'step_id' => intval( $step_id ),
1963 'sections' => $sections,
1964 'message' => esc_html__( 'Thank you sections updated successfully.', 'cartflows' ),
1965 );
1966
1967 } catch ( \Exception $e ) {
1968 return $this->error( $e );
1969 }
1970 }
1971
1972 /**
1973 * Update thank you redirect settings.
1974 *
1975 * @param array $input Validated input.
1976 * @return array
1977 * @throws \Exception On failure.
1978 */
1979 public function update_thankyou_redirect( $input ) {
1980
1981 try {
1982 $this->init( $input, CARTFLOWS_ABILITY_API_NAMESPACE . 'update-thankyou-redirect' );
1983
1984 if ( ! function_exists( 'WC' ) ) {
1985 throw new \Exception( esc_html__( 'WooCommerce is required for thank you abilities.', 'cartflows' ) );
1986 }
1987
1988 $step_id = $this->input_get( 'step_id' );
1989
1990 if ( null === $step_id || 0 === intval( $step_id ) ) {
1991 throw new \Exception( esc_html__( 'Invalid step ID.', 'cartflows' ) );
1992 }
1993
1994 if ( get_post_type( $step_id ) !== CARTFLOWS_STEP_POST_TYPE ) {
1995 throw new \Exception( esc_html__( 'Step not found.', 'cartflows' ) );
1996 }
1997
1998 $step_type = get_post_meta( $step_id, 'wcf-step-type', true );
1999 if ( 'thankyou' !== $step_type ) {
2000 throw new \Exception( esc_html__( 'Step is not a thank you type.', 'cartflows' ) );
2001 }
2002
2003 $enabled = $this->input_get( 'enabled', null );
2004 $redirect_url = $this->input_get( 'redirect_url', null );
2005 $updated = false;
2006
2007 if ( null !== $enabled && '' !== $enabled ) {
2008 update_post_meta( $step_id, 'wcf-show-tq-redirect-section', sanitize_text_field( $enabled ) );
2009 $updated = true;
2010 }
2011
2012 if ( null !== $redirect_url && '' !== $redirect_url ) {
2013 update_post_meta( $step_id, 'wcf-tq-redirect-link', esc_url_raw( $redirect_url ) );
2014 $updated = true;
2015 }
2016
2017 if ( ! $updated ) {
2018 throw new \Exception( esc_html__( 'No redirect settings provided to update.', 'cartflows' ) );
2019 }
2020
2021 return array(
2022 'step_id' => intval( $step_id ),
2023 'enabled' => esc_html( wcf()->options->get_thankyou_meta_value( $step_id, 'wcf-show-tq-redirect-section' ) ),
2024 'redirect_url' => esc_url( wcf()->options->get_thankyou_meta_value( $step_id, 'wcf-tq-redirect-link' ) ),
2025 'message' => esc_html__( 'Thank you redirect settings updated successfully.', 'cartflows' ),
2026 );
2027
2028 } catch ( \Exception $e ) {
2029 return $this->error( $e );
2030 }
2031 }
2032
2033 /**
2034 * Update thank you custom text.
2035 *
2036 * @param array $input Validated input.
2037 * @return array
2038 * @throws \Exception On failure.
2039 */
2040 public function update_thankyou_custom_text( $input ) {
2041
2042 try {
2043 $this->init( $input, CARTFLOWS_ABILITY_API_NAMESPACE . 'update-thankyou-custom-text' );
2044
2045 if ( ! function_exists( 'WC' ) ) {
2046 throw new \Exception( esc_html__( 'WooCommerce is required for thank you abilities.', 'cartflows' ) );
2047 }
2048
2049 $step_id = $this->input_get( 'step_id' );
2050 $text = $this->input_get( 'text' );
2051
2052 if ( null === $step_id || 0 === intval( $step_id ) ) {
2053 throw new \Exception( esc_html__( 'Invalid step ID.', 'cartflows' ) );
2054 }
2055
2056 if ( get_post_type( $step_id ) !== CARTFLOWS_STEP_POST_TYPE ) {
2057 throw new \Exception( esc_html__( 'Step not found.', 'cartflows' ) );
2058 }
2059
2060 $step_type = get_post_meta( $step_id, 'wcf-step-type', true );
2061 if ( 'thankyou' !== $step_type ) {
2062 throw new \Exception( esc_html__( 'Step is not a thank you type.', 'cartflows' ) );
2063 }
2064
2065 update_post_meta( $step_id, 'wcf-tq-text', sanitize_text_field( $text ) );
2066
2067 return array(
2068 'step_id' => intval( $step_id ),
2069 'text' => esc_html( get_post_meta( $step_id, 'wcf-tq-text', true ) ),
2070 'message' => esc_html__( 'Thank you custom text updated successfully.', 'cartflows' ),
2071 );
2072
2073 } catch ( \Exception $e ) {
2074 return $this->error( $e );
2075 }
2076 }
2077
2078 // ============================================================
2079 // Execute Callbacks — Optin Read
2080 // ============================================================
2081
2082 /**
2083 * Get optin step settings: product, button text, pass-fields config.
2084 *
2085 * @param array $input Validated input.
2086 * @return array
2087 * @throws \Exception On failure.
2088 */
2089 public function get_optin_settings( $input ) {
2090
2091 try {
2092 $this->init( $input, CARTFLOWS_ABILITY_API_NAMESPACE . 'get-optin-settings' );
2093
2094 if ( ! function_exists( 'WC' ) ) {
2095 throw new \Exception( esc_html__( 'WooCommerce is required for optin abilities.', 'cartflows' ) );
2096 }
2097
2098 $step_id = $this->input_get( 'step_id' );
2099
2100 if ( null === $step_id || 0 === intval( $step_id ) ) {
2101 throw new \Exception( esc_html__( 'Invalid step ID.', 'cartflows' ) );
2102 }
2103
2104 if ( get_post_type( $step_id ) !== CARTFLOWS_STEP_POST_TYPE ) {
2105 throw new \Exception( esc_html__( 'Step not found.', 'cartflows' ) );
2106 }
2107
2108 $step_type = get_post_meta( $step_id, 'wcf-step-type', true );
2109 if ( 'optin' !== $step_type ) {
2110 throw new \Exception( esc_html__( 'Step is not an optin type.', 'cartflows' ) );
2111 }
2112
2113 // Product.
2114 $product_raw = wcf()->options->get_optin_meta_value( $step_id, 'wcf-optin-product' );
2115 $product = array(
2116 'id' => 0,
2117 'name' => '',
2118 );
2119
2120 if ( ! empty( $product_raw ) ) {
2121 $product_id = is_array( $product_raw ) ? intval( reset( $product_raw ) ) : intval( $product_raw );
2122 $product_obj = ( $product_id > 0 ) ? wc_get_product( $product_id ) : null;
2123
2124 if ( $product_obj ) {
2125 $product = array(
2126 'id' => $product_id,
2127 'name' => esc_html( $product_obj->get_name() ),
2128 );
2129 }
2130 }
2131
2132 // Button text.
2133 $button_text = wcf()->options->get_optin_meta_value( $step_id, 'wcf-submit-button-text' );
2134
2135 // Pass fields.
2136 $pass_fields = array(
2137 'enabled' => esc_html( wcf()->options->get_optin_meta_value( $step_id, 'wcf-optin-pass-fields' ) ),
2138 'specific_fields' => esc_html( wcf()->options->get_optin_meta_value( $step_id, 'wcf-optin-pass-specific-fields' ) ),
2139 );
2140
2141 return array(
2142 'step_id' => intval( $step_id ),
2143 'product' => $product,
2144 'button_text' => esc_html( $button_text ),
2145 'pass_fields' => $pass_fields,
2146 );
2147
2148 } catch ( \Exception $e ) {
2149 return $this->error( $e );
2150 }
2151 }
2152
2153 // ============================================================
2154 // Execute Callbacks — Optin Write
2155 // ============================================================
2156
2157 /**
2158 * Update optin product assignment.
2159 *
2160 * @param array $input Validated input.
2161 * @return array
2162 * @throws \Exception On failure.
2163 */
2164 public function update_optin_product( $input ) {
2165
2166 try {
2167 $this->init( $input, CARTFLOWS_ABILITY_API_NAMESPACE . 'update-optin-product' );
2168
2169 if ( ! function_exists( 'WC' ) ) {
2170 throw new \Exception( esc_html__( 'WooCommerce is required for optin abilities.', 'cartflows' ) );
2171 }
2172
2173 $step_id = $this->input_get( 'step_id' );
2174 $product_id = $this->input_get( 'product_id' );
2175
2176 if ( null === $step_id || 0 === intval( $step_id ) ) {
2177 throw new \Exception( esc_html__( 'Invalid step ID.', 'cartflows' ) );
2178 }
2179
2180 if ( get_post_type( $step_id ) !== CARTFLOWS_STEP_POST_TYPE ) {
2181 throw new \Exception( esc_html__( 'Step not found.', 'cartflows' ) );
2182 }
2183
2184 $step_type = get_post_meta( $step_id, 'wcf-step-type', true );
2185 if ( 'optin' !== $step_type ) {
2186 throw new \Exception( esc_html__( 'Step is not an optin type.', 'cartflows' ) );
2187 }
2188
2189 if ( $product_id <= 0 ) {
2190 throw new \Exception( esc_html__( 'Invalid product ID.', 'cartflows' ) );
2191 }
2192
2193 $product_obj = wc_get_product( $product_id );
2194 if ( ! $product_obj ) {
2195 throw new \Exception( esc_html__( 'WooCommerce product not found.', 'cartflows' ) );
2196 }
2197
2198 if ( ! $product_obj->is_type( 'simple' ) ) {
2199 throw new \Exception( esc_html__( 'Product must be a Simple product type.', 'cartflows' ) );
2200 }
2201
2202 if ( ! $product_obj->is_virtual() ) {
2203 throw new \Exception( esc_html__( 'Product must be Virtual.', 'cartflows' ) );
2204 }
2205
2206 if ( $product_obj->get_price() > 0 ) {
2207 throw new \Exception( esc_html__( 'Product price must be zero (free).', 'cartflows' ) );
2208 }
2209
2210 update_post_meta( $step_id, 'wcf-optin-product', array( $product_id ) );
2211
2212 return array(
2213 'step_id' => intval( $step_id ),
2214 'product_id' => intval( $product_id ),
2215 'product_name' => esc_html( $product_obj->get_name() ),
2216 'message' => esc_html__( 'Optin product updated successfully.', 'cartflows' ),
2217 );
2218
2219 } catch ( \Exception $e ) {
2220 return $this->error( $e );
2221 }
2222 }
2223
2224 /**
2225 * Update optin submit button text.
2226 *
2227 * @param array $input Validated input.
2228 * @return array
2229 * @throws \Exception On failure.
2230 */
2231 public function update_optin_button_text( $input ) {
2232
2233 try {
2234 $this->init( $input, CARTFLOWS_ABILITY_API_NAMESPACE . 'update-optin-button-text' );
2235
2236 if ( ! function_exists( 'WC' ) ) {
2237 throw new \Exception( esc_html__( 'WooCommerce is required for optin abilities.', 'cartflows' ) );
2238 }
2239
2240 $step_id = $this->input_get( 'step_id' );
2241 $button_text = $this->input_get( 'button_text' );
2242
2243 if ( null === $step_id || 0 === intval( $step_id ) ) {
2244 throw new \Exception( esc_html__( 'Invalid step ID.', 'cartflows' ) );
2245 }
2246
2247 if ( get_post_type( $step_id ) !== CARTFLOWS_STEP_POST_TYPE ) {
2248 throw new \Exception( esc_html__( 'Step not found.', 'cartflows' ) );
2249 }
2250
2251 $step_type = get_post_meta( $step_id, 'wcf-step-type', true );
2252 if ( 'optin' !== $step_type ) {
2253 throw new \Exception( esc_html__( 'Step is not an optin type.', 'cartflows' ) );
2254 }
2255
2256 if ( empty( $button_text ) ) {
2257 throw new \Exception( esc_html__( 'Button text cannot be empty.', 'cartflows' ) );
2258 }
2259
2260 update_post_meta( $step_id, 'wcf-submit-button-text', sanitize_text_field( $button_text ) );
2261
2262 return array(
2263 'step_id' => intval( $step_id ),
2264 'button_text' => esc_html( get_post_meta( $step_id, 'wcf-submit-button-text', true ) ),
2265 'message' => esc_html__( 'Optin button text updated successfully.', 'cartflows' ),
2266 );
2267
2268 } catch ( \Exception $e ) {
2269 return $this->error( $e );
2270 }
2271 }
2272
2273 /**
2274 * Update optin pass-fields settings.
2275 *
2276 * @param array $input Validated input.
2277 * @return array
2278 * @throws \Exception On failure.
2279 */
2280 public function update_optin_pass_fields( $input ) {
2281
2282 try {
2283 $this->init( $input, CARTFLOWS_ABILITY_API_NAMESPACE . 'update-optin-pass-fields' );
2284
2285 if ( ! function_exists( 'WC' ) ) {
2286 throw new \Exception( esc_html__( 'WooCommerce is required for optin abilities.', 'cartflows' ) );
2287 }
2288
2289 $step_id = $this->input_get( 'step_id' );
2290
2291 if ( null === $step_id || 0 === intval( $step_id ) ) {
2292 throw new \Exception( esc_html__( 'Invalid step ID.', 'cartflows' ) );
2293 }
2294
2295 if ( get_post_type( $step_id ) !== CARTFLOWS_STEP_POST_TYPE ) {
2296 throw new \Exception( esc_html__( 'Step not found.', 'cartflows' ) );
2297 }
2298
2299 $step_type = get_post_meta( $step_id, 'wcf-step-type', true );
2300 if ( 'optin' !== $step_type ) {
2301 throw new \Exception( esc_html__( 'Step is not an optin type.', 'cartflows' ) );
2302 }
2303
2304 $enabled = $this->input_get( 'enabled', null );
2305 $specific_fields = $this->input_get( 'specific_fields', null );
2306 $updated = false;
2307
2308 if ( null !== $enabled && '' !== $enabled ) {
2309 update_post_meta( $step_id, 'wcf-optin-pass-fields', sanitize_text_field( $enabled ) );
2310 $updated = true;
2311 }
2312
2313 if ( null !== $specific_fields && '' !== $specific_fields ) {
2314 update_post_meta( $step_id, 'wcf-optin-pass-specific-fields', sanitize_text_field( $specific_fields ) );
2315 $updated = true;
2316 }
2317
2318 if ( ! $updated ) {
2319 throw new \Exception( esc_html__( 'No pass-fields settings provided to update.', 'cartflows' ) );
2320 }
2321
2322 return array(
2323 'step_id' => intval( $step_id ),
2324 'enabled' => esc_html( wcf()->options->get_optin_meta_value( $step_id, 'wcf-optin-pass-fields' ) ),
2325 'specific_fields' => esc_html( wcf()->options->get_optin_meta_value( $step_id, 'wcf-optin-pass-specific-fields' ) ),
2326 'message' => esc_html__( 'Optin pass-fields settings updated successfully.', 'cartflows' ),
2327 );
2328
2329 } catch ( \Exception $e ) {
2330 return $this->error( $e );
2331 }
2332 }
2333
2334 // ── Landing ─────────────────────────────────────────────────
2335
2336 /**
2337 * Return the configuration of a landing step.
2338 *
2339 * Fields returned: slug, next_step_link, disable_step.
2340 *
2341 * @param array $input Ability input containing step_id.
2342 * @return array|WP_Error
2343 * @throws \Exception On failure.
2344 */
2345 public function get_landing_settings( $input ) {
2346
2347 try {
2348
2349 $this->init( $input, CARTFLOWS_ABILITY_API_NAMESPACE . 'get-landing-settings' );
2350
2351 $step_id = $this->input_get( 'step_id' );
2352
2353 if ( ! $step_id || 'cartflows_step' !== get_post_type( $step_id ) ) {
2354 throw new \Exception( esc_html__( 'Invalid step ID.', 'cartflows' ) );
2355 }
2356
2357 $step_type = get_post_meta( $step_id, 'wcf-step-type', true );
2358
2359 if ( 'landing' !== $step_type ) {
2360 throw new \Exception( esc_html__( 'The provided step is not a landing step.', 'cartflows' ) );
2361 }
2362
2363 $slug = get_post_field( 'post_name', $step_id );
2364 $next_step_link = wcf()->utils->get_linking_url(
2365 array( 'id' => $step_id )
2366 );
2367 $disable_step = get_post_meta( $step_id, 'wcf-disable-step', true );
2368
2369 return array(
2370 'step_id' => intval( $step_id ),
2371 'slug' => esc_html( $slug ),
2372 'next_step_link' => esc_url( $next_step_link ),
2373 'disable_step' => esc_html( ! empty( $disable_step ) ? $disable_step : 'no' ),
2374 );
2375
2376 } catch ( \Exception $e ) {
2377 return $this->error( $e );
2378 }
2379 }
2380
2381 // ============================================================
2382 // Execute Callbacks — Email Report
2383 // ============================================================
2384
2385 /**
2386 * Get email report settings: enabled status, recipient list, next scheduled time.
2387 *
2388 * @param array $input Validated input.
2389 * @return array
2390 * @throws \Exception On failure.
2391 */
2392 public function get_email_report_settings( $input ) {
2393
2394 try {
2395
2396 $this->init( $input, CARTFLOWS_ABILITY_API_NAMESPACE . 'get-email-report-settings' );
2397
2398 $enabled = get_option( 'cartflows_stats_report_emails', 'enable' );
2399 $email_raw = get_option( 'cartflows_stats_report_email_ids', get_option( 'admin_email' ) );
2400
2401 $email_ids = array();
2402 if ( ! empty( $email_raw ) ) {
2403 $email_ids = array_values( array_filter( array_map( 'trim', preg_split( "/[\f\r\n]+/", $email_raw ) ) ) );
2404 }
2405
2406 $next_scheduled = '';
2407 if ( function_exists( 'as_next_scheduled_action' ) ) {
2408 $timestamp = as_next_scheduled_action( 'cartflows_send_report_summary_email' );
2409 if ( ! empty( $timestamp ) && false !== $timestamp ) {
2410 $next_scheduled = gmdate( 'c', $timestamp );
2411 }
2412 }
2413
2414 return array(
2415 'enabled' => esc_html( $enabled ),
2416 'email_ids' => array_map( 'sanitize_email', $email_ids ),
2417 'next_scheduled' => esc_html( $next_scheduled ),
2418 );
2419
2420 } catch ( \Exception $e ) {
2421 return $this->error( $e );
2422 }
2423 }
2424
2425 /**
2426 * Update email report settings: toggle enabled, update recipient list.
2427 *
2428 * @param array $input Validated input.
2429 * @return array
2430 * @throws \Exception On failure.
2431 */
2432 public function update_email_report_settings( $input ) {
2433
2434 try {
2435
2436 $this->init( $input, CARTFLOWS_ABILITY_API_NAMESPACE . 'update-email-report-settings' );
2437
2438 $enabled = $this->input_get( 'enabled', '' );
2439 $email_ids = $this->input_get( 'email_ids', array() );
2440
2441 if ( ! empty( $enabled ) ) {
2442 $enabled = in_array( $enabled, array( 'enable', 'disable' ), true ) ? $enabled : 'enable';
2443 update_option( 'cartflows_stats_report_emails', $enabled );
2444 }
2445
2446 if ( ! empty( $email_ids ) && is_array( $email_ids ) ) {
2447 $sanitized = array_values( array_filter( array_map( 'sanitize_email', $email_ids ) ) );
2448 $email_str = implode( "\n", $sanitized );
2449 update_option( 'cartflows_stats_report_email_ids', $email_str );
2450 }
2451
2452 // Read back saved values.
2453 $saved_enabled = get_option( 'cartflows_stats_report_emails', 'enable' );
2454 $saved_email_raw = get_option( 'cartflows_stats_report_email_ids', get_option( 'admin_email' ) );
2455 $saved_email_ids = array();
2456 if ( ! empty( $saved_email_raw ) ) {
2457 $saved_email_ids = array_values( array_filter( array_map( 'trim', preg_split( "/[\f\r\n]+/", $saved_email_raw ) ) ) );
2458 }
2459
2460 return array(
2461 'enabled' => esc_html( $saved_enabled ),
2462 'email_ids' => array_map( 'sanitize_email', $saved_email_ids ),
2463 'message' => esc_html__( 'Email report settings updated.', 'cartflows' ),
2464 );
2465
2466 } catch ( \Exception $e ) {
2467 return $this->error( $e );
2468 }
2469 }
2470
2471 // ============================================================
2472 // Execute Callbacks — Woo Dynamic Flow
2473 // ============================================================
2474
2475 /**
2476 * Get flow mapping for a single WooCommerce product.
2477 *
2478 * @param array $input Validated input.
2479 * @return array
2480 * @throws \Exception On failure.
2481 */
2482 public function get_product_flow_mapping( $input ) {
2483
2484 try {
2485
2486 $this->init( $input, CARTFLOWS_ABILITY_API_NAMESPACE . 'get-product-flow-mapping' );
2487
2488 if ( ! class_exists( 'WooCommerce' ) ) {
2489 throw new \Exception( esc_html__( 'WooCommerce is not active.', 'cartflows' ) );
2490 }
2491
2492 $product_id = $this->input_get( 'product_id' );
2493 $product = wc_get_product( $product_id );
2494
2495 if ( ! $product ) {
2496 throw new \Exception( esc_html__( 'Invalid product ID.', 'cartflows' ) );
2497 }
2498
2499 $flow_id = intval( $product->get_meta( 'cartflows_redirect_flow_id' ) );
2500 $flow_title = '';
2501 $add_to_cart_text = $product->get_meta( 'cartflows_add_to_cart_text' );
2502
2503 if ( ! empty( $flow_id ) && get_post_type( $flow_id ) === CARTFLOWS_FLOW_POST_TYPE ) {
2504 $flow_title = get_the_title( $flow_id );
2505 }
2506
2507 return array(
2508 'product_id' => intval( $product_id ),
2509 'product_title' => esc_html( $product->get_name() ),
2510 'flow_id' => $flow_id,
2511 'flow_title' => esc_html( $flow_title ),
2512 'add_to_cart_text' => esc_html( wp_unslash( $add_to_cart_text ) ),
2513 );
2514
2515 } catch ( \Exception $e ) {
2516 return $this->error( $e );
2517 }
2518 }
2519
2520 /**
2521 * List all WooCommerce products that have a flow mapping.
2522 *
2523 * @param array $input Validated input.
2524 * @return array
2525 * @throws \Exception On failure.
2526 */
2527 public function list_product_flow_mappings( $input ) {
2528
2529 try {
2530
2531 $this->init( $input, CARTFLOWS_ABILITY_API_NAMESPACE . 'list-product-flow-mappings' );
2532
2533 if ( ! class_exists( 'WooCommerce' ) ) {
2534 throw new \Exception( esc_html__( 'WooCommerce is not active.', 'cartflows' ) );
2535 }
2536
2537 $page = $this->clamp_page( $this->input_get( 'paged' ) );
2538 $per_page = $this->clamp_per_page( $this->input_get( 'per_page' ) );
2539
2540 $query = new \WP_Query(
2541 array(
2542 'post_type' => 'product',
2543 'post_status' => 'publish',
2544 'posts_per_page' => $per_page,
2545 'paged' => $page,
2546 'meta_query' => array( // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
2547 array(
2548 'key' => 'cartflows_redirect_flow_id',
2549 'value' => array( '', '0' ),
2550 'compare' => 'NOT IN',
2551 ),
2552 ),
2553 'fields' => 'ids',
2554 )
2555 );
2556
2557 $mappings = array();
2558
2559 if ( ! empty( $query->posts ) ) {
2560 foreach ( $query->posts as $pid ) {
2561 $product = wc_get_product( $pid );
2562 if ( ! $product ) {
2563 continue;
2564 }
2565
2566 $flow_id = intval( $product->get_meta( 'cartflows_redirect_flow_id' ) );
2567 $flow_title = '';
2568 if ( ! empty( $flow_id ) && get_post_type( $flow_id ) === CARTFLOWS_FLOW_POST_TYPE ) {
2569 $flow_title = get_the_title( $flow_id );
2570 }
2571
2572 $mappings[] = array(
2573 'product_id' => intval( $pid ),
2574 'product_title' => esc_html( $product->get_name() ),
2575 'flow_id' => $flow_id,
2576 'flow_title' => esc_html( $flow_title ),
2577 'add_to_cart_text' => esc_html( wp_unslash( $product->get_meta( 'cartflows_add_to_cart_text' ) ) ),
2578 );
2579 }
2580 }
2581
2582 $total = intval( $query->found_posts );
2583 $total_pages = intval( $query->max_num_pages );
2584
2585 return array(
2586 'mappings' => $mappings,
2587 'total' => $total,
2588 'total_pages' => $total_pages,
2589 'page' => $page,
2590 );
2591
2592 } catch ( \Exception $e ) {
2593 return $this->error( $e );
2594 }
2595 }
2596
2597 /**
2598 * Update flow mapping on a WooCommerce product.
2599 *
2600 * @param array $input Validated input.
2601 * @return array
2602 * @throws \Exception On failure.
2603 */
2604 public function update_product_flow_mapping( $input ) {
2605
2606 try {
2607
2608 $this->init( $input, CARTFLOWS_ABILITY_API_NAMESPACE . 'update-product-flow-mapping' );
2609
2610 if ( ! class_exists( 'WooCommerce' ) ) {
2611 throw new \Exception( esc_html__( 'WooCommerce is not active.', 'cartflows' ) );
2612 }
2613
2614 $product_id = $this->input_get( 'product_id' );
2615 $product = wc_get_product( $product_id );
2616
2617 if ( ! $product ) {
2618 throw new \Exception( esc_html__( 'Invalid product ID.', 'cartflows' ) );
2619 }
2620
2621 $flow_id = $this->input_get( 'flow_id', '' );
2622 $add_to_cart_text = $this->input_get( 'add_to_cart_text', '' );
2623
2624 if ( '' !== $flow_id ) {
2625 $flow_id = intval( $flow_id );
2626
2627 // Validate flow exists if non-zero.
2628 if ( $flow_id > 0 && get_post_type( $flow_id ) !== CARTFLOWS_FLOW_POST_TYPE ) {
2629 throw new \Exception( esc_html__( 'Invalid flow ID. Flow does not exist.', 'cartflows' ) );
2630 }
2631
2632 $product->update_meta_data( 'cartflows_redirect_flow_id', $flow_id );
2633 }
2634
2635 if ( '' !== $add_to_cart_text ) {
2636 $product->update_meta_data( 'cartflows_add_to_cart_text', sanitize_text_field( $add_to_cart_text ) );
2637 }
2638
2639 $product->save();
2640
2641 // Read back saved values.
2642 $saved_flow_id = intval( $product->get_meta( 'cartflows_redirect_flow_id' ) );
2643 $saved_flow_title = '';
2644 if ( ! empty( $saved_flow_id ) && get_post_type( $saved_flow_id ) === CARTFLOWS_FLOW_POST_TYPE ) {
2645 $saved_flow_title = get_the_title( $saved_flow_id );
2646 }
2647 $saved_button_text = $product->get_meta( 'cartflows_add_to_cart_text' );
2648
2649 return array(
2650 'product_id' => intval( $product_id ),
2651 'flow_id' => $saved_flow_id,
2652 'flow_title' => esc_html( $saved_flow_title ),
2653 'add_to_cart_text' => esc_html( wp_unslash( $saved_button_text ) ),
2654 'message' => esc_html__( 'Product flow mapping updated.', 'cartflows' ),
2655 );
2656
2657 } catch ( \Exception $e ) {
2658 return $this->error( $e );
2659 }
2660 }
2661
2662 // ============================================================
2663 // Execute Callbacks — Flow Creation / Import
2664 // ============================================================
2665
2666 /**
2667 * Create a new empty funnel.
2668 *
2669 * @param array $input Validated input.
2670 * @return array
2671 * @throws \Exception On failure.
2672 */
2673 public function create_flow( $input ) {
2674
2675 try {
2676 $this->init( $input, CARTFLOWS_ABILITY_API_NAMESPACE . 'create-flow' );
2677
2678 $title = $this->input_get( 'title' );
2679 $status = $this->input_get( 'status' );
2680
2681 if ( empty( $title ) ) {
2682 throw new \Exception( esc_html__( 'Funnel title cannot be empty.', 'cartflows' ) );
2683 }
2684
2685 $new_flow_id = wp_insert_post(
2686 array(
2687 'post_type' => CARTFLOWS_FLOW_POST_TYPE,
2688 'post_title' => sanitize_text_field( $title ),
2689 'post_status' => $status,
2690 'post_content' => '',
2691 )
2692 );
2693
2694 if ( is_wp_error( $new_flow_id ) || ! $new_flow_id ) {
2695 throw new \Exception( esc_html__( 'Failed to create funnel.', 'cartflows' ) );
2696 }
2697
2698 update_post_meta( $new_flow_id, 'wcf-steps', array() );
2699
2700 return array(
2701 'flow_id' => intval( $new_flow_id ),
2702 'title' => esc_html( get_the_title( $new_flow_id ) ),
2703 'status' => esc_html( get_post_status( $new_flow_id ) ),
2704 'edit_url' => esc_url( admin_url( 'admin.php?page=cartflows&path=flows&action=wcf-edit-flow&flow_id=' . $new_flow_id ) ),
2705 );
2706
2707 } catch ( \Exception $e ) {
2708 return $this->error( $e );
2709 }
2710 }
2711
2712 /**
2713 * Create a new step and attach it to a funnel.
2714 *
2715 * @param array $input Validated input.
2716 * @return array
2717 * @throws \Exception On failure.
2718 */
2719 public function create_step( $input ) {
2720
2721 try {
2722 $this->init( $input, CARTFLOWS_ABILITY_API_NAMESPACE . 'create-step' );
2723
2724 $flow_id = $this->input_get( 'flow_id' );
2725 $title = $this->input_get( 'title' );
2726 $type = $this->input_get( 'type' );
2727
2728 if ( 0 === $flow_id ) {
2729 throw new \Exception( esc_html__( 'Invalid funnel ID.', 'cartflows' ) );
2730 }
2731
2732 if ( CARTFLOWS_FLOW_POST_TYPE !== get_post_type( $flow_id ) ) {
2733 throw new \Exception( esc_html__( 'Funnel not found.', 'cartflows' ) );
2734 }
2735
2736 if ( empty( $title ) ) {
2737 throw new \Exception( esc_html__( 'Step title cannot be empty.', 'cartflows' ) );
2738 }
2739
2740 // Upsell/downsell require CartFlows Pro.
2741 if ( in_array( $type, array( 'upsell', 'downsell' ), true ) ) {
2742 throw new \Exception( esc_html__( 'Upsell and Downsell steps require CartFlows Pro.', 'cartflows' ) );
2743 }
2744
2745 $new_step_id = wp_insert_post(
2746 array(
2747 'post_type' => CARTFLOWS_STEP_POST_TYPE,
2748 'post_title' => sanitize_text_field( $title ),
2749 'post_status' => 'publish',
2750 )
2751 );
2752
2753 if ( is_wp_error( $new_step_id ) || ! $new_step_id ) {
2754 throw new \Exception( esc_html__( 'Failed to create step.', 'cartflows' ) );
2755 }
2756
2757 // Set step meta.
2758 update_post_meta( $new_step_id, 'wcf-flow-id', $flow_id );
2759 update_post_meta( $new_step_id, 'wcf-step-type', $type );
2760
2761 // Set taxonomy terms.
2762 wp_set_object_terms( $new_step_id, $type, CARTFLOWS_TAXONOMY_STEP_TYPE );
2763 wp_set_object_terms( $new_step_id, 'flow-' . $flow_id, CARTFLOWS_TAXONOMY_STEP_FLOW );
2764
2765 // Append to flow's wcf-steps meta.
2766 $flow_steps = get_post_meta( $flow_id, 'wcf-steps', true );
2767 if ( ! is_array( $flow_steps ) ) {
2768 $flow_steps = array();
2769 }
2770
2771 $flow_steps[] = array(
2772 'id' => intval( $new_step_id ),
2773 'title' => sanitize_text_field( $title ),
2774 'type' => $type,
2775 );
2776
2777 if ( method_exists( '\Cartflows_Helper', 'get_instance' ) ) {
2778 $flow_steps = \Cartflows_Helper::get_instance()->maybe_update_flow_steps( $flow_id, $flow_steps );
2779 }
2780
2781 update_post_meta( $flow_id, 'wcf-steps', $flow_steps );
2782 AdminHelper::clear_cache();
2783
2784 return array(
2785 'step_id' => intval( $new_step_id ),
2786 'flow_id' => intval( $flow_id ),
2787 'title' => esc_html( get_the_title( $new_step_id ) ),
2788 'type' => esc_html( $type ),
2789 'edit_url' => esc_url( admin_url( 'admin.php?page=cartflows&path=flows&action=wcf-edit-flow&flow_id=' . $flow_id ) ),
2790 );
2791
2792 } catch ( \Exception $e ) {
2793 return $this->error( $e );
2794 }
2795 }
2796
2797 // ============================================================
2798 // Execute Callbacks — Templates Browse
2799 // ============================================================
2800
2801 /**
2802 * List available flow templates from the cached library.
2803 *
2804 * @param array $input Validated input.
2805 * @return array
2806 * @throws \Exception On failure.
2807 */
2808 public function list_flow_templates( $input ) {
2809
2810 try {
2811 $this->init( $input, CARTFLOWS_ABILITY_API_NAMESPACE . 'list-flow-templates' );
2812
2813 $type = $this->input_get( 'type' );
2814 $page_builder = $this->input_get( 'page_builder' );
2815 $step_type = $this->input_get( 'step_type' );
2816 $category = $this->input_get( 'category' );
2817 $search = $this->input_get( 'search' );
2818 $page = $this->clamp_page( $this->input_get( 'page' ) );
2819 $per_page = $this->clamp_per_page( $this->input_get( 'per_page' ) );
2820
2821 // Use provided page builder or fall back to the default.
2822 if ( empty( $page_builder ) ) {
2823 $page_builder = \Cartflows_Helper::get_common_setting( 'default_page_builder' );
2824 }
2825
2826 $all_flows = \Cartflows_Helper::get_instance()->get_flows_and_steps( sanitize_text_field( $page_builder ) );
2827
2828 if ( ! is_array( $all_flows ) ) {
2829 $all_flows = array();
2830 }
2831
2832 $templates = array();
2833
2834 foreach ( $all_flows as $flow ) {
2835 $flow = (array) $flow;
2836
2837 $flow_id = isset( $flow['ID'] ) ? intval( $flow['ID'] ) : 0;
2838 $flow_title = isset( $flow['title'] ) ? $flow['title'] : '';
2839 $flow_type = isset( $flow['type'] ) ? $flow['type'] : '';
2840 $is_pro = 'pro' === $flow_type;
2841 $thumbnail = isset( $flow['featured_image_url'] ) ? $flow['featured_image_url'] : '';
2842 $pb = isset( $flow['page_builder'] ) ? $flow['page_builder'] : $page_builder;
2843 $categories = isset( $flow['category'] ) ? (array) $flow['category'] : array();
2844
2845 // Get step types in this flow.
2846 $flow_steps = isset( $flow['steps'] ) ? (array) $flow['steps'] : array();
2847 $flow_step_types = array();
2848 foreach ( $flow_steps as $step ) {
2849 $step = (array) $step;
2850 if ( isset( $step['type'] ) ) {
2851 $flow_step_types[] = $step['type'];
2852 }
2853 }
2854
2855 // Filter by type (free/pro) if provided.
2856 if ( ! empty( $type ) && $flow_type !== $type ) {
2857 continue;
2858 }
2859
2860 // Filter by step type if provided.
2861 if ( ! empty( $step_type ) && ! in_array( $step_type, $flow_step_types, true ) ) {
2862 continue;
2863 }
2864
2865 // Filter by category if provided.
2866 if ( ! empty( $category ) && ! in_array( $category, $categories, true ) ) {
2867 continue;
2868 }
2869
2870 // Filter by search keyword if provided.
2871 if ( ! empty( $search ) && false === stripos( $flow_title, $search ) ) {
2872 continue;
2873 }
2874
2875 $templates[] = array(
2876 'id' => $flow_id,
2877 'title' => esc_html( $flow_title ),
2878 'type' => esc_html( $flow_type ),
2879 'page_builder' => esc_html( $pb ),
2880 'is_pro' => $is_pro,
2881 'categories' => array_map( 'sanitize_text_field', $categories ),
2882 'step_count' => count( $flow_steps ),
2883 'step_types' => array_map( 'sanitize_text_field', $flow_step_types ),
2884 'thumbnail_url' => esc_url( (string) $thumbnail ),
2885 );
2886 }
2887
2888 $total = count( $templates );
2889 $offset = ( $page - 1 ) * $per_page;
2890 $paged = array_slice( $templates, $offset, $per_page );
2891
2892 return array(
2893 'templates' => $paged,
2894 'total' => $total,
2895 'page' => $page,
2896 'per_page' => $per_page,
2897 'pro_available' => _is_cartflows_pro(),
2898 );
2899
2900 } catch ( \Exception $e ) {
2901 return $this->error( $e );
2902 }
2903 }
2904
2905 // ============================================================
2906 // Execute Callbacks — Templates Import
2907 // ============================================================
2908
2909 /**
2910 * Import a flow template from the CartFlows cloud library.
2911 *
2912 * @param array $input Validated input.
2913 * @return array
2914 * @throws \Exception On failure.
2915 */
2916 public function import_flow_template( $input ) {
2917
2918 try {
2919 $this->init( $input, CARTFLOWS_ABILITY_API_NAMESPACE . 'import-flow-template' );
2920
2921 if ( ! function_exists( 'WC' ) ) {
2922 throw new \Exception( esc_html__( 'WooCommerce is required to import flow templates.', 'cartflows' ) );
2923 }
2924
2925 $template_id = $this->input_get( 'template_id' );
2926
2927 if ( 0 === $template_id ) {
2928 throw new \Exception( esc_html__( 'Invalid template ID.', 'cartflows' ) );
2929 }
2930
2931 // Fetch the flow template from the CartFlows cloud API.
2932 $response = \CartFlows_API::get_instance()->get_flow( $template_id );
2933
2934 if ( ! isset( $response['success'] ) || ! $response['success'] ) {
2935 $message = isset( $response['message'] ) ? $response['message'] : __( 'Failed to fetch template from CartFlows library.', 'cartflows' );
2936 throw new \Exception( esc_html( $message ) );
2937 }
2938
2939 // Check for API errors.
2940 $is_error = AdminHelper::has_api_error( $response['data'] );
2941 if ( $is_error['error'] ) {
2942 throw new \Exception( esc_html( $is_error['error_message'] ) );
2943 }
2944
2945 // Check licence status for Pro templates.
2946 $license_status = isset( $response['data']['licence_status'] ) ? $response['data']['licence_status'] : '';
2947
2948 if ( 'valid' !== $license_status && ! _is_cartflows_pro() ) {
2949 throw new \Exception( esc_html__( 'This template requires CartFlows Pro. Please upgrade to import premium templates.', 'cartflows' ) );
2950 }
2951
2952 // Extract the flow title.
2953 $flow_title = isset( $response['title'] ) ? sanitize_text_field( $response['title'] ) : __( 'Imported Flow', 'cartflows' );
2954
2955 // Create the flow post.
2956 $new_flow_id = wp_insert_post(
2957 array(
2958 'post_type' => CARTFLOWS_FLOW_POST_TYPE,
2959 'post_title' => $flow_title,
2960 'post_status' => 'publish',
2961 'post_content' => '',
2962 )
2963 );
2964
2965 if ( is_wp_error( $new_flow_id ) || ! $new_flow_id ) {
2966 throw new \Exception( esc_html__( 'Failed to create funnel from template.', 'cartflows' ) );
2967 }
2968
2969 update_post_meta( $new_flow_id, 'wcf-steps', array() );
2970
2971 // Import flow post meta if available.
2972 if ( ! empty( $response['post_meta'] ) ) {
2973 $exclude_meta_keys = \Cartflows_Helper::get_instance()->get_meta_keys_to_exclude_from_import( $new_flow_id );
2974 foreach ( (array) $response['post_meta'] as $meta_key => $meta_value ) {
2975 if ( in_array( $meta_key, $exclude_meta_keys, true ) ) {
2976 continue;
2977 }
2978 $meta_value = isset( $meta_value[0] ) ? $meta_value[0] : '';
2979 if ( $meta_value ) {
2980 if ( is_serialized( $meta_value, true ) ) {
2981 $raw_data = unserialize( stripslashes( $meta_value ), array( 'allowed_classes' => false ) ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_unserialize, PHPCompatibility.FunctionUse.NewFunctionParameters.unserialize_optionsFound
2982 if ( false === $raw_data || is_object( $raw_data ) ) {
2983 $raw_data = '';
2984 }
2985 } else {
2986 $raw_data = $meta_value;
2987 }
2988 update_post_meta( $new_flow_id, $meta_key, $raw_data );
2989 }
2990 }
2991 }
2992
2993 // Process steps from the template.
2994 // Get the steps list from the locally cached flows_and_steps data,
2995 // matching how the working AJAX import_flow() method gets steps from the frontend.
2996 // The remote API response does not contain steps in a reliably iterable format.
2997 $steps_raw = array();
2998 $all_flows_data = \Cartflows_Helper::get_instance()->get_flows_and_steps();
2999
3000 if ( ! empty( $all_flows_data ) && is_array( $all_flows_data ) ) {
3001 foreach ( $all_flows_data as $cached_flow ) {
3002 $cached_flow = (array) $cached_flow;
3003 $cached_flow_id = isset( $cached_flow['ID'] ) ? absint( $cached_flow['ID'] ) : 0;
3004
3005 if ( $cached_flow_id === $template_id ) {
3006 if ( isset( $cached_flow['steps'] ) ) {
3007 // Convert each step from stdClass to array if needed.
3008 foreach ( $cached_flow['steps'] as $cached_step ) {
3009 $steps_raw[] = (array) $cached_step;
3010 }
3011 }
3012 break;
3013 }
3014 }
3015 }
3016
3017 // Fallback: try the remote API response if cached data did not yield steps.
3018 if ( empty( $steps_raw ) ) {
3019 $template_data = isset( $response['data'] ) ? $response['data'] : array();
3020
3021 // cartflows_flow_flow may be a stdClass from json_decode, so cast it.
3022 if ( isset( $template_data['cartflows_flow_flow'] ) && ! empty( $template_data['cartflows_flow_flow'] ) ) {
3023 foreach ( (array) $template_data['cartflows_flow_flow'] as $api_step ) {
3024 $steps_raw[] = (array) $api_step;
3025 }
3026 }
3027 }
3028
3029 $imported_steps = array();
3030 $skipped_steps = array();
3031 $importer = \CartFlows_Importer::get_instance();
3032
3033 // Set the batch import flag before processing steps.
3034 \CartFlows_Batch_Process::set_is_wcf_template_import( true );
3035
3036 foreach ( $steps_raw as $step ) {
3037 $step = (array) $step;
3038
3039 $step_remote_id = isset( $step['ID'] ) ? absint( $step['ID'] ) : ( isset( $step['id'] ) ? absint( $step['id'] ) : 0 );
3040 $step_title = isset( $step['title'] ) ? sanitize_text_field( $step['title'] ) : '';
3041 $step_type = isset( $step['type'] ) ? sanitize_text_field( $step['type'] ) : '';
3042
3043 // Skip upsell/downsell if Pro is not active.
3044 if ( in_array( $step_type, array( 'upsell', 'downsell' ), true ) && ( ! _is_cartflows_pro() || is_wcf_starter_plan() ) ) {
3045 $skipped_steps[] = array(
3046 'title' => esc_html( $step_title ),
3047 'type' => esc_html( $step_type ),
3048 'reason' => esc_html__( 'Upsell and Downsell steps require CartFlows Pro.', 'cartflows' ),
3049 );
3050 continue;
3051 }
3052
3053 if ( empty( $step_remote_id ) ) {
3054 continue;
3055 }
3056
3057 // Use the importer to create the step locally.
3058 $new_step_id = $importer->create_step( $new_flow_id, $step_type, $step_title );
3059
3060 if ( ! empty( $new_step_id ) && ! is_wp_error( $new_step_id ) ) {
3061 // Fetch and import the step template content.
3062 $step_response = \CartFlows_API::get_instance()->get_template( $step_remote_id );
3063
3064 if ( isset( $step_response['success'] ) && $step_response['success'] ) {
3065 // Handle Divi content.
3066 if ( 'divi' === \Cartflows_Helper::get_common_setting( 'default_page_builder' ) ) {
3067 if ( isset( $step_response['data']['divi_content'] ) && ! empty( $step_response['data']['divi_content'] ) ) {
3068 update_post_meta( $new_step_id, 'divi_content', $step_response['data']['divi_content'] );
3069 wp_update_post(
3070 array(
3071 'ID' => $new_step_id,
3072 'post_content' => $step_response['data']['divi_content'],
3073 )
3074 );
3075 }
3076 }
3077
3078 // Handle Gutenberg content.
3079 if ( 'gutenberg' === \Cartflows_Helper::get_common_setting( 'default_page_builder' ) ) {
3080 if ( isset( $step_response['data']['divi_content'] ) && ! empty( $step_response['data']['divi_content'] ) ) {
3081 wp_update_post(
3082 array(
3083 'ID' => $new_step_id,
3084 'post_content' => $step_response['data']['divi_content'],
3085 )
3086 );
3087 }
3088 }
3089
3090 // Import step post meta.
3091 if ( ! empty( $step_response['post_meta'] ) ) {
3092 $step_exclude_keys = \Cartflows_Helper::get_instance()->get_meta_keys_to_exclude_from_import( $new_step_id );
3093 foreach ( (array) $step_response['post_meta'] as $meta_key => $meta_value ) {
3094 if ( in_array( $meta_key, $step_exclude_keys, true ) ) {
3095 continue;
3096 }
3097 $meta_value = isset( $meta_value[0] ) ? $meta_value[0] : '';
3098 if ( $meta_value ) {
3099 if ( is_serialized( $meta_value, true ) ) {
3100 $raw_data = unserialize( stripslashes( $meta_value ), array( 'allowed_classes' => false ) ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_unserialize, PHPCompatibility.FunctionUse.NewFunctionParameters.unserialize_optionsFound
3101 if ( false === $raw_data || is_object( $raw_data ) ) {
3102 $raw_data = '';
3103 }
3104 } elseif ( '_elementor_data' === $meta_key ) {
3105 $raw_data = is_array( $meta_value ) ? wp_slash( wp_json_encode( $meta_value ) ) : wp_slash( $meta_value );
3106 } else {
3107 $raw_data = $meta_value;
3108 }
3109 update_post_meta( $new_step_id, $meta_key, $raw_data );
3110 }
3111 }
3112 }
3113
3114 update_post_meta( $new_step_id, 'cartflows_imported_step', 'yes' );
3115
3116 do_action( 'cartflows_import_complete' );
3117 do_action( 'cartflows_after_template_import', $new_step_id, $step_response );
3118 }
3119
3120 $imported_steps[] = array(
3121 'step_id' => intval( $new_step_id ),
3122 'title' => esc_html( get_the_title( $new_step_id ) ),
3123 'type' => esc_html( $step_type ),
3124 );
3125 }
3126 }
3127
3128 // Reset the batch import flag after processing steps.
3129 \CartFlows_Batch_Process::set_is_wcf_template_import( false );
3130
3131 return array(
3132 'flow_id' => intval( $new_flow_id ),
3133 'title' => esc_html( get_the_title( $new_flow_id ) ),
3134 'steps' => $imported_steps,
3135 'skipped_steps' => $skipped_steps,
3136 );
3137
3138 } catch ( \Exception $e ) {
3139 return $this->error( $e );
3140 }
3141 }
3142
3143 // ============================================================
3144 // Private Helpers
3145 // ============================================================
3146
3147 /**
3148 * Set the post_status on a flow and all its steps.
3149 *
3150 * @param int $flow_id Flow post ID.
3151 * @param string $new_status New post status ('publish', 'draft', etc.).
3152 * @return void
3153 */
3154 private function set_flow_status( $flow_id, $new_status ) {
3155
3156 $steps = get_post_meta( $flow_id, 'wcf-steps', true );
3157
3158 if ( is_array( $steps ) ) {
3159 foreach ( $steps as $step ) {
3160 wp_update_post(
3161 array(
3162 'ID' => intval( $step['id'] ),
3163 'post_status' => $new_status,
3164 )
3165 );
3166 }
3167 }
3168
3169 wp_update_post(
3170 array(
3171 'ID' => $flow_id,
3172 'post_status' => $new_status,
3173 )
3174 );
3175 }
3176
3177 // ============================================================
3178 // Base Helpers (required by template)
3179 // ============================================================
3180
3181 /**
3182 * Initialize input parsing for the given ability.
3183 *
3184 * @param array $input Raw input.
3185 * @param string $ability_name Fully-qualified ability name.
3186 * @return void
3187 */
3188 public function init( $input, $ability_name ) {
3189 $this->input_parse( $input, $ability_name );
3190 }
3191
3192 /**
3193 * Check whether the current user has the given capability/capabilities.
3194 *
3195 * @param string|array $caps Single capability string or array (AND logic).
3196 * @return bool
3197 */
3198 public function permission_callback( $caps ) {
3199
3200 if ( empty( $caps ) ) {
3201 return false;
3202 }
3203
3204 $user = wp_get_current_user();
3205 if ( ! $user || 0 === $user->ID ) {
3206 return false;
3207 }
3208
3209 if ( is_string( $caps ) ) {
3210 return (bool) $user->has_cap( $caps );
3211 }
3212
3213 if ( is_array( $caps ) ) {
3214 foreach ( $caps as $cap ) {
3215 if ( ! $user->has_cap( $cap ) ) {
3216 return false;
3217 }
3218 }
3219 return true;
3220 }
3221
3222 return false;
3223 }
3224
3225 /**
3226 * Parse and validate input against the ability's input_schema.
3227 *
3228 * Applies defaults, coerces types, sanitizes, and validates enums.
3229 *
3230 * @param array $input Raw input.
3231 * @param string $ability_name Fully-qualified ability name.
3232 * @return array Parsed input.
3233 * @throws \Exception On failure.
3234 */
3235 public function input_parse( $input, $ability_name ) {
3236
3237 $this->input = array();
3238
3239 if ( is_a( $input, 'WP_REST_Request' ) ) {
3240 $input = $input->get_json_params();
3241 if ( ! is_array( $input ) ) {
3242 $input = array();
3243 }
3244 }
3245
3246 if ( ! is_array( $input ) ) {
3247 $input = array();
3248 }
3249
3250 $input_schema = Cartflows_Ability_Config::get_ability_input_schema( $ability_name );
3251 if ( ! is_array( $input_schema ) || empty( $input_schema ) ) {
3252 return array();
3253 }
3254
3255 if ( ! isset( $input_schema['properties'] ) || ! is_array( $input_schema['properties'] ) ) {
3256 return array();
3257 }
3258
3259 // Required fields: array at object level per JSON Schema standard.
3260 $required_fields = ( isset( $input_schema['required'] ) && is_array( $input_schema['required'] ) )
3261 ? $input_schema['required']
3262 : array();
3263
3264 foreach ( $input_schema['properties'] as $name => $prop ) {
3265
3266 $type = isset( $prop['type'] ) ? strtolower( $prop['type'] ) : 'string';
3267 $raw_value = array_key_exists( $name, $input ) ? $input[ $name ] : null;
3268
3269 // Required check BEFORE defaults.
3270 $is_required = in_array( $name, $required_fields, true );
3271 if ( $is_required && ( null === $raw_value || '' === $raw_value ) ) {
3272 throw new \Exception(
3273 sprintf(
3274 /* translators: %s: field name */
3275 esc_html__( 'Required field %s is missing.', 'cartflows' ),
3276 esc_html( $name )
3277 )
3278 );
3279 }
3280
3281 // Apply schema default if value not provided.
3282 if ( null === $raw_value && isset( $prop['default'] ) ) {
3283 $raw_value = $prop['default'];
3284 }
3285
3286 // Type-appropriate zero value if still null.
3287 if ( null === $raw_value ) {
3288 switch ( $type ) {
3289 case 'integer':
3290 $raw_value = 0;
3291 break;
3292 case 'number':
3293 $raw_value = 0.0;
3294 break;
3295 case 'boolean':
3296 $raw_value = false;
3297 break;
3298 case 'array':
3299 $raw_value = array();
3300 break;
3301 case 'object':
3302 $raw_value = array();
3303 break;
3304 default:
3305 $raw_value = '';
3306 break;
3307 }
3308 }
3309
3310 $value = $raw_value;
3311
3312 // Type coercion and sanitization.
3313 switch ( $type ) {
3314 case 'integer':
3315 $value = intval( $value );
3316 break;
3317 case 'number':
3318 $value = floatval( $value );
3319 break;
3320 case 'boolean':
3321 $value = filter_var( $value, FILTER_VALIDATE_BOOLEAN );
3322 break;
3323 case 'string':
3324 $value = is_string( $value )
3325 ? sanitize_text_field( $value )
3326 : sanitize_text_field( strval( $value ) );
3327 break;
3328 case 'array':
3329 if ( ! is_array( $value ) ) {
3330 $value = array();
3331 }
3332 $value = $this->sanitize_recursive( $value );
3333 break;
3334 case 'object':
3335 if ( ! is_array( $value ) && ! is_object( $value ) ) {
3336 $value = array();
3337 }
3338 if ( is_object( $value ) ) {
3339 $value = (array) $value;
3340 }
3341 $value = $this->sanitize_recursive( $value );
3342 break;
3343 }
3344
3345 // Enum validation.
3346 if ( isset( $prop['enum'] ) && is_array( $prop['enum'] ) ) {
3347 if ( ! in_array( $value, $prop['enum'], true ) ) {
3348 throw new \Exception(
3349 sprintf(
3350 /* translators: %s: field name */
3351 esc_html__( 'Invalid value for %s.', 'cartflows' ),
3352 esc_html( $name )
3353 )
3354 );
3355 }
3356 }
3357
3358 $this->input[ $name ] = $value;
3359 }
3360
3361 return $this->input;
3362 }
3363
3364 /**
3365 * Get a parsed input value by name.
3366 *
3367 * @param string $name Property name.
3368 * @param mixed $default Sentinel: omit to throw on missing; pass a value to use as fallback.
3369 * @return mixed
3370 * @throws \Exception On failure.
3371 */
3372 public function input_get( $name, $default = '__CARTFLOWS_NO_DEFAULT__' ) {
3373
3374 if ( false === $this->input ) {
3375 throw new \Exception( esc_html__( 'Inputs not parsed.', 'cartflows' ) );
3376 }
3377
3378 if ( ! array_key_exists( $name, $this->input ) ) {
3379 if ( '__CARTFLOWS_NO_DEFAULT__' !== $default ) {
3380 return $default;
3381 }
3382 throw new \Exception(
3383 sprintf(
3384 /* translators: %s: property name */
3385 esc_html__( 'Property %s not found.', 'cartflows' ),
3386 esc_html( $name )
3387 )
3388 );
3389 }
3390
3391 return $this->input[ $name ];
3392 }
3393
3394 /**
3395 * Recursively sanitize an array.
3396 *
3397 * @param array $data Data to sanitize.
3398 * @return array
3399 */
3400 protected function sanitize_recursive( $data ) {
3401
3402 if ( ! is_array( $data ) ) {
3403 return $data;
3404 }
3405
3406 $sanitized = array();
3407 foreach ( $data as $key => $value ) {
3408 $key = sanitize_text_field( strval( $key ) );
3409 if ( is_array( $value ) ) {
3410 $sanitized[ $key ] = $this->sanitize_recursive( $value );
3411 } elseif ( is_string( $value ) ) {
3412 $sanitized[ $key ] = sanitize_text_field( $value );
3413 } elseif ( is_int( $value ) ) {
3414 $sanitized[ $key ] = intval( $value );
3415 } elseif ( is_float( $value ) ) {
3416 $sanitized[ $key ] = floatval( $value );
3417 } elseif ( is_bool( $value ) ) {
3418 $sanitized[ $key ] = (bool) $value;
3419 } else {
3420 $sanitized[ $key ] = $value;
3421 }
3422 }
3423
3424 return $sanitized;
3425 }
3426
3427 /**
3428 * Build a structured error response.
3429 *
3430 * Only exposes file/line info when WP_DEBUG is enabled.
3431 *
3432 * @param \Exception $e The exception.
3433 * @return array
3434 */
3435 public function error( $e ) {
3436
3437 $error = array(
3438 'error' => array(
3439 'code' => 'cartflows_error',
3440 'message' => esc_html( $e->getMessage() ),
3441 ),
3442 );
3443
3444 if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
3445 $error['error']['debug'] = array(
3446 'file' => $e->getFile(),
3447 'line' => $e->getLine(),
3448 );
3449 }
3450
3451 return $error;
3452 }
3453
3454 /**
3455 * Clamp per_page to safe bounds (1–$max).
3456 *
3457 * @param int $per_page Raw per_page value.
3458 * @param int $max Maximum allowed (default 100).
3459 * @return int
3460 */
3461 protected function clamp_per_page( $per_page, $max = 100 ) {
3462 return max( 1, min( intval( $per_page ), $max ) );
3463 }
3464
3465 /**
3466 * Clamp page to minimum 1.
3467 *
3468 * @param int $page Raw page value.
3469 * @return int
3470 */
3471 protected function clamp_page( $page ) {
3472 return max( 1, intval( $page ) );
3473 }
3474 }
3475