PluginProbe
WPFunnels – Funnel Builder for WooCommerce with Checkout & One Click Upsell / 3.10.9
WPFunnels – Funnel Builder for WooCommerce with Checkout & One Click Upsell v3.10.9
3.13.3 3.13.2 3.13.1 3.13.0 3.12.13 3.12.12 3.12.11 3.12.10 3.12.9 3.12.8 3.12.7 3.12.6 3.12.5 3.12.4 3.12.3 3.12.1 3.12.2 3.12.0 3.11.1 3.11.0 3.10.9 3.10.8 3.10.7 3.10.6 2.8.16 All 261 releases
wpfunnels / public / modules / checkout / class-wpfnl-checkout.php

class-wpfnl-checkout.php in WPFunnels – Funnel Builder for WooCommerce with Checkout & One Click Upsell 3.10.9, at public/modules/checkout/class-wpfnl-checkout.php

2,230 lines 74.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Checkout module
4 *
5 * @package
6 * @since 2.7.9
7 */
8
9 namespace WPFunnels\Modules\Frontend\Checkout;
10
11 use Error;
12 use WC_Session_Handler;
13 use WPFunnels\Data_Store\Wpfnl_Steps_Store_Data;
14 use WPFunnels\Frontend\Module\Wpfnl_Frontend_Module;
15 use WPFunnels\Frontend\Wpfnl_Public;
16 use WPFunnels\Meta\Wpfnl_Default_Meta;
17 use WPFunnels\Wpfnl;
18 use WPFunnels\Wpfnl_functions;
19 use WPFunnels\lms\helper\Wpfnl_lms_learndash_functions;
20 use WPFunnels\Modules\Frontend\Checkout\Variable\Wpfnl_Variable_Product;
21 use WPFunnels\Modules\Frontend\Checkout\Single\Wpfnl_Single_Product;
22 use function cli\err;
23 use Wpfnl_Public_Type_Factory;
24 use WPFunnels\Conditions\Wpfnl_Condition_Checker;
25 use WPFunnels\Discount\WpfnlDiscount;
26 use WPFunnels\Modules\Frontend\CheckoutHelper\CheckoutHelper;
27
28 // Include Product Selection class
29 require_once WPFNL_DIR . 'public/modules/checkout/class-wpfnl-product-selection.php';
30
31 class Module extends Wpfnl_Frontend_Module
32 {
33 public $funnel_id;
34
35 public $step_id;
36
37 // const CHECKOUT = "";
38
39 public function __construct()
40 {
41
42 /* set checkout flag */
43 add_filter('woocommerce_is_checkout', [$this, 'checkout_flag'], 9999);
44
45 /* suppress WC "session expired" AJAX response when rendering checkout with empty cart for preview */
46 add_filter('woocommerce_checkout_update_order_review_expired', [$this, 'allow_empty_cart_order_review']);
47
48 if( !defined('WCML_VERSION') ){
49 add_action('woocommerce_checkout_update_order_meta', [$this, 'save_checkout_fields'], 10, 2);
50 }
51
52 /* initialize cart data */
53 add_action( 'wp', [ $this, 'initialize_cart_data' ] );
54
55 /* init WooCommerce action */
56 add_action( 'wp', [$this, 'init_actions']);
57
58 /* Exclude Funnel Builder hooks when WPFunnels checkout is active */
59 add_action( 'wp', [$this, 'exclude_funnel_builder_hooks'], 1 );
60
61 /* Remove CartFlows global checkout override before it fires on 'wp' priority 0 */
62 add_action( 'template_redirect', [$this, 'exclude_funnel_builder_hooks'], 0 );
63
64 /* Exclude conflicting hooks during checkout AJAX (update_order_review) */
65 add_action( 'woocommerce_checkout_update_order_review', [$this, 'exclude_funnel_builder_hooks_on_ajax'], -1 );
66
67 /*
68 * Prevent CartFlows from overriding the global checkout on WPFunnels step pages.
69 * CartFlows calls should_show_global_checkout() inside override_global_checkout()
70 * and global_checkout_template_redirect() — returning true from this filter
71 * makes both methods bail out immediately, regardless of object reference issues.
72 */
73 add_filter( 'cartflows_allow_display_global_checkout', [$this, 'prevent_cartflows_global_checkout_override'], 10, 2 );
74
75 /*
76 * Prevent FunnelKit from redirecting to its own global checkout on WPFunnels
77 * store checkout pages. fk_checkout_redirect_resolve fires on this filter and
78 * WFACP_Common caches the result — intercept early at priority 5 (before FK's
79 * priority 8 and 10) and return 0 to signal "no override" when on a WPFunnels page.
80 */
81 add_filter( 'wfacp_global_checkout_page_id', [$this, 'prevent_funnelkit_global_checkout_override'], 5, 1 );
82
83 /* register checkout shortcode */
84 add_shortcode('wpfunnels_checkout', array($this, 'render_checkout_shortcode'));
85 add_action('wp_ajax_wpfnl_next_button_ajax', [$this, 'wpfnl_next_button_ajax']);
86 add_action('wp_ajax_nopriv_wpfnl_next_button_ajax', [$this, 'wpfnl_next_button_ajax']);
87
88 add_action( 'wp_ajax_wpfnl_order_bump_ajax', [$this, 'wpfnl_order_bump_ajax']);
89 add_action( 'wp_ajax_nopriv_wpfnl_order_bump_ajax', [$this, 'wpfnl_order_bump_ajax']);
90 add_action( 'wp_ajax_wpfnl_get_course_details', [$this, 'wpfnl_get_course_details']);
91 add_action( 'wp_ajax_nopriv_wpfnl_get_course_details', [$this, 'wpfnl_get_course_details']);
92
93 add_action( 'wp_ajax_wpfnl_update_variation', [$this, 'wpfnl_update_variation']);
94 add_action( 'wp_ajax_nopriv_wpfnl_update_variation', [$this, 'wpfnl_update_variation']);
95 add_action( 'wp_ajax_wpfnl_apply_checkout_coupon', [$this, 'wpfnl_apply_checkout_coupon'] );
96 add_action( 'wp_ajax_nopriv_wpfnl_apply_checkout_coupon', [$this, 'wpfnl_apply_checkout_coupon'] );
97 add_action( 'wp_ajax_wpfnl_remove_checkout_coupon', [$this, 'wpfnl_remove_checkout_coupon'] );
98 add_action( 'wp_ajax_nopriv_wpfnl_remove_checkout_coupon', [$this, 'wpfnl_remove_checkout_coupon'] );
99
100 // Email existence checker for modern checkout customer-info flow.
101 add_action( 'wp_ajax_wpf_check_email_exists', [$this, 'wpfnl_check_email_exists'] );
102 add_action( 'wp_ajax_nopriv_wpf_check_email_exists', [$this, 'wpfnl_check_email_exists'] );
103 add_action( 'wp_ajax_wpfnl_check_email_exists', [$this, 'wpfnl_check_email_exists'] );
104 add_action( 'wp_ajax_nopriv_wpfnl_check_email_exists', [$this, 'wpfnl_check_email_exists'] );
105
106 add_action( 'woocommerce_before_calculate_totals', [$this, 'custom_price_to_cart_item'], 9999);
107 add_action( 'woocommerce_before_checkout_form', [$this, 'apply_auto_coupon'], 9999);
108
109 add_action( 'wp_ajax_nopriv_wpfnl_checkout_cart', [$this, 'wpfnl_checkout_cart']);
110 add_filter('woocommerce_cart_product_out_of_stock_message', [$this,'wpfnl_out_of_stock_message_in_checkout'], 10, 2);
111
112 add_action( 'wpfunnels/before_checkout_form', array($this, 'before_checkout_form_actions') );
113
114 add_action( 'woocommerce_after_order_notes', array( $this, 'wpfnl_set_checkout_id' ), 999 );
115 add_action( 'woocommerce_login_form_end', array( $this, 'wpfnl_set_checkout_id' ), 999 );
116 add_filter( 'woocommerce_login_redirect', [$this,'wpfnl_redirect_after_login'], 10, 2 );
117 add_action( 'woocommerce_before_calculate_totals', [$this,'wpfnl_checkout_discount'], 10);
118 add_action( 'woocommerce_cart_calculate_fees', [$this,'wpfnl_checkout_discount'], 10);
119
120 add_action( 'wpfunnels/after_optin_submit', array( $this, 'get_optin_data_checkout' ), 10, 4 );
121 add_filter( 'woocommerce_checkout_fields', array($this, 'set_option_data_in_checkout_filed'),10 );
122 add_filter( 'woocommerce_available_payment_gateways', array($this, 'conditional_payment_gateways'),10 );
123 add_action( 'woocommerce_checkout_after_customer_details', array( $this, 'add_custom_shipping_section' ), 10 );
124 add_action( 'woocommerce_checkout_order_review', array( $this, 'display_custom_coupon_field' ) );
125
126 /* Manage Express Checkout buttons visibility dynamically */
127 add_filter( 'wc_stripe_show_payment_request_on_checkout', [$this, 'wpfnl_manage_stripe_express_checkout'], 9999, 2 );
128
129 /* Position Express Checkout buttons based on step setting */
130 add_action( 'wp', [$this, 'wpfnl_position_stripe_express_checkout'], 5 );
131
132 }
133
134 /**
135 * Manage Stripe Express Checkout buttons visibility based on WPFunnels step settings.
136 *
137 * @param bool $show If it should show
138 * @param \WP_Post $post Current post
139 * @return bool
140 */
141 public function wpfnl_manage_stripe_express_checkout( $show, $post = null ) {
142 if ( ! Wpfnl_functions::check_if_this_is_step_type('checkout') ) {
143 return $show;
144 }
145
146 $checkout_id = '';
147 if( wp_doing_ajax() ){
148 $checkout_id = Wpfnl_functions::get_checkout_id_from_post($_POST);
149 } else {
150 global $post;
151 if ( $post && isset( $post->ID ) ) {
152 $checkout_id = $post->ID;
153 }
154 }
155
156 if ( $checkout_id ) {
157 $express_checkout_enabled = get_post_meta( $checkout_id, '_wpfnl_express_checkout_enabled', true );
158 if ( 'yes' !== $express_checkout_enabled ) {
159 return false; // Hide Stripe Payment Request (Apple Pay / Google Pay)
160 }
161 }
162
163 return $show;
164 }
165
166 /**
167 * Reposition Stripe Express Checkout buttons based on the `_wpfnl_express_checkout_position`
168 * step meta. Detaches Stripe's default hook and re-attaches it to the user-selected position.
169 *
170 * Supported positions:
171 * - top -> woocommerce_checkout_before_customer_details (default Stripe pos)
172 * - before_product_switcher -> woocommerce_before_order_notes, priority 4
173 * - after_product_switcher -> woocommerce_before_order_notes, priority 6
174 * - before_order_summary -> woocommerce_checkout_before_order_review
175 * - after_order_summary -> woocommerce_checkout_after_order_review
176 * - above_payment_gateways -> woocommerce_review_order_before_payment
177 *
178 * @return void
179 */
180 public function wpfnl_position_stripe_express_checkout() {
181 if ( ! Wpfnl_functions::check_if_this_is_step_type('checkout') ) {
182 return;
183 }
184
185 global $post;
186 $checkout_id = $post && isset( $post->ID ) ? $post->ID : 0;
187 if ( ! $checkout_id ) {
188 return;
189 }
190
191 $express_checkout_enabled = get_post_meta( $checkout_id, '_wpfnl_express_checkout_enabled', true );
192 // Only reposition when the feature is enabled. 'yes' is the default.
193 if ( 'yes' !== $express_checkout_enabled ) {
194 return;
195 }
196
197 $position = get_post_meta( $checkout_id, '_wpfnl_express_checkout_position', true );
198 if ( ! $position ) {
199 $position = 'top'; // default
200 }
201
202 // No repositioning needed for the default 'top' position — Stripe already hooks there.
203 if ( 'top' === $position ) {
204 return;
205 }
206
207 // Retrieve the Stripe ECE singleton (hooks are already registered since we are on 'wp' action).
208 if ( ! class_exists( '\WC_Stripe_Express_Checkout_Element' ) ) {
209 return;
210 }
211
212 $ece = \WC_Stripe_Express_Checkout_Element::instance();
213 if ( ! $ece ) {
214 return;
215 }
216
217 // Detach from Stripe's default checkout position.
218 remove_action( 'woocommerce_checkout_before_customer_details', [ $ece, 'display_express_checkout_button_html' ], 1 );
219
220 // Map setting → hook name + priority.
221 $hook_map = array(
222 'before_product_switcher' => array( 'woocommerce_before_order_notes', 4 ),
223 'after_product_switcher' => array( 'woocommerce_before_order_notes', 6 ),
224 'before_order_summary' => array( 'woocommerce_checkout_before_order_review', 10 ),
225 'after_order_summary' => array( 'woocommerce_checkout_after_order_review', 10 ),
226 'above_payment_gateways' => array( 'woocommerce_review_order_before_payment', 10 ),
227 );
228
229 if ( isset( $hook_map[ $position ] ) ) {
230 list( $hook, $priority ) = $hook_map[ $position ];
231 add_action( $hook, [ $ece, 'display_express_checkout_button_html' ], $priority );
232 }
233 }
234
235
236
237
238 /**
239 * Set checkout flag to true if this is checkout step type
240 *
241 * @param $is_checkout
242 *
243 * @return bool
244 *
245 * @since 1.0.0
246 */
247 public function checkout_flag($is_checkout)
248 {
249 if (Wpfnl_functions::check_if_this_is_step_type('checkout') && ! Wpfnl_functions::check_if_this_is_lms_step() ) {
250 $is_checkout = true;
251 }
252 return $is_checkout;
253 }
254
255
256 /**
257 * Prevent WooCommerce from returning "session expired" on the update_order_review
258 * AJAX when the cart is empty because we're rendering a Store Checkout preview.
259 *
260 * @param bool $expired
261 * @return bool
262 */
263 public function allow_empty_cart_order_review( $expired ) {
264 if ( WC()->session && WC()->session->get( 'wpfnl_checkout_preview_mode' ) ) {
265 return false;
266 }
267 return $expired;
268 }
269
270 /**
271 * Prevent CartFlows from overriding the global checkout page when the current
272 * page is a WPFunnels step. CartFlows checks this filter inside both
273 * override_global_checkout() and global_checkout_template_redirect() — returning
274 * true makes both bail out immediately without touching $GLOBALS['post'].
275 *
276 * @param bool $is_allow
277 * @param int $checkout_id Current post ID passed by CartFlows.
278 * @return bool
279 * @since 3.10.6
280 */
281 public function prevent_cartflows_global_checkout_override( $is_allow, $checkout_id ) {
282 if ( $is_allow ) {
283 return $is_allow;
284 }
285
286 // If the current post is a WPFunnels step, tell CartFlows to skip its override.
287 if ( $checkout_id && Wpfnl_functions::is_funnel_step_page() ) {
288 return true;
289 }
290
291 return $is_allow;
292 }
293
294 /**
295 * Prevent FunnelKit from redirecting to its own global checkout on WPFunnels
296 * store checkout pages.
297 *
298 * fk_checkout_redirect_resolve (priority 10) and maybe_override_global_checkout_id
299 * (priority 8) both fire on wfacp_global_checkout_page_id after this callback.
300 * WordPress passes each callback's return value to the next, so returning 0 here
301 * is not enough — priority 8/10 will overwrite it with a real step ID.
302 *
303 * The fix: when on a WPFunnels step page, remove those downstream filters from
304 * inside this callback (they haven't run yet at priority 5), then return 0.
305 * WFACP_Common treats 0 as "no global checkout configured" → skips the redirect.
306 *
307 * @param int $checkout_page_id
308 * @return int
309 * @since 3.10.6
310 */
311 public function prevent_funnelkit_global_checkout_override( $checkout_page_id ) {
312 if ( ! Wpfnl_functions::is_funnel_step_page() ) {
313 return $checkout_page_id;
314 }
315
316 // Remove FunnelKit's downstream filters before they run (we are at priority 5).
317 if ( class_exists( 'WFFN_Step_WC_Checkout' ) ) {
318 $wffn_checkout = \WFFN_Step_WC_Checkout::get_instance();
319 if ( $wffn_checkout ) {
320 remove_filter( 'wfacp_global_checkout_page_id', array( $wffn_checkout, 'maybe_override_global_checkout_id' ), 8 );
321 remove_filter( 'wfacp_global_checkout_page_id', array( $wffn_checkout, 'fk_checkout_redirect_resolve' ), 10 );
322 }
323 }
324
325 // Return 0 — WFACP_Common treats 0 as "no global checkout configured".
326 return 0;
327 }
328
329
330 /**
331 * Remove conflicting hooks from Funnel Builder (WFACP/FunnelKit) and CartFlows
332 * that interfere with WPFunnels checkout pages.
333 *
334 * Two entry points:
335 * 1. 'template_redirect' (priority -1) — fires before 'wp', catches CartFlows
336 * global checkout override which runs at wp priority 0.
337 * 2. 'wp' (priority 1) — catches remaining hooks that need $post to be set.
338 *
339 * @since 3.10.6
340 */
341 public function exclude_funnel_builder_hooks() {
342 // Bail early if not a WPFunnels step page at all
343 if ( ! Wpfnl_functions::is_funnel_step_page() ) {
344 return;
345 }
346
347 $this->remove_conflicting_plugin_hooks();
348 }
349
350 /**
351 * Exclude conflicting hooks during the woocommerce_checkout_update_order_review
352 * AJAX call, where is_funnel_step_page() cannot rely on get_post_type().
353 * Uses _wpfunnels_checkout_id in POST data to detect WPFunnels context.
354 *
355 * @since 3.10.6
356 */
357 public function exclude_funnel_builder_hooks_on_ajax() {
358 $checkout_id = Wpfnl_functions::get_checkout_id_from_post( $_POST );
359 if ( ! $checkout_id ) {
360 return;
361 }
362 if ( ! Wpfnl_functions::check_if_this_is_step_type_by_id( $checkout_id, 'checkout' ) ) {
363 return;
364 }
365
366 $this->remove_conflicting_plugin_hooks();
367 }
368
369 /**
370 * Core removal logic shared by both page-load and AJAX entry points.
371 *
372 * @since 3.10.6
373 */
374 private function remove_conflicting_plugin_hooks() {
375 // --- FunnelKit (WFACP) ---
376 if ( class_exists( 'WFFN_Step_WC_Checkout' ) ) {
377 $wffn_checkout = \WFFN_Step_WC_Checkout::get_instance();
378 if ( $wffn_checkout ) {
379 // Prevents FunnelKit store checkout from overriding WPFunnels checkout page ID
380 remove_filter( 'wfacp_global_checkout_page_id', array( $wffn_checkout, 'maybe_override_global_checkout_id' ), 8 );
381 // Prevents FK cart-based checkout redirect from firing on WPFunnels pages
382 remove_filter( 'wfacp_global_checkout_page_id', array( $wffn_checkout, 'fk_checkout_redirect_resolve' ), 10 );
383 // Prevents FunnelKit from setting up its own funnel session on WPFunnels AJAX
384 remove_action( 'woocommerce_checkout_update_order_review', array( $wffn_checkout, 'setup_funnel_on_update_order' ), 99 );
385 }
386 }
387
388 // --- CartFlows: global checkout page override ---
389 if ( class_exists( 'Cartflows_Global_Checkout' ) ) {
390 $cartflows_global = \Cartflows_Global_Checkout::get_instance();
391 if ( $cartflows_global ) {
392 // Prevents CartFlows from replacing $GLOBALS['post'] with its own checkout
393 remove_action( 'wp', array( $cartflows_global, 'override_global_checkout' ), 0 );
394 remove_action( 'wp', array( $cartflows_global, 'maybe_override_order_pay_page' ), 0 );
395 // Prevents CartFlows from redirecting to its global checkout URL
396 remove_action( 'template_redirect', array( $cartflows_global, 'global_checkout_template_redirect' ), 1 );
397 }
398 }
399
400 // --- CartFlows: checkout markup hooks ---
401 if ( class_exists( 'Cartflows_Checkout_Markup' ) ) {
402 $cartflows_markup = \Cartflows_Checkout_Markup::get_instance();
403 if ( $cartflows_markup ) {
404 // Prevents CartFlows from hijacking the woocommerce_is_checkout flag
405 remove_filter( 'woocommerce_is_checkout', array( $cartflows_markup, 'woo_checkout_flag' ), 9999 );
406 // Prevents CartFlows from emptying and repopulating the cart with its own products
407 remove_action( 'wp', array( $cartflows_markup, 'preconfigured_cart_data' ), 1 );
408 // Prevents CartFlows from loading its scripts/hooks on WPFunnels pages
409 remove_action( 'wp', array( $cartflows_markup, 'shortcode_load_data' ), 999 );
410 // Prevents CartFlows from restoring a different checkout's cart during AJAX
411 remove_action( 'woocommerce_checkout_update_order_review', array( $cartflows_markup, 'restore_cart_data' ) );
412 // Prevents CartFlows from overwriting WPFunnels cart item prices
413 remove_action( 'woocommerce_before_calculate_totals', array( $cartflows_markup, 'custom_price_to_cart_item' ), 9999 );
414 }
415 }
416 }
417
418
419 /**
420 * Init necessary actions related Checkout step
421 */
422 public function init_actions() {
423 add_action( 'woocommerce_after_order_notes', [$this, 'checkout_shortcode_metas']);
424 $this->display_coupon_field();
425 $this->enable_checkout_hook();
426
427 if ( Wpfnl_functions::check_if_this_is_step_type( 'checkout' ) ) {
428 add_action( 'wp_enqueue_scripts', array( $this, 'load_checkout_step_scripts' ) );
429 }
430 }
431
432
433 /**
434 * Load script/styles related to checkout step
435 *
436 * @since 3.1.3
437 */
438 public function load_checkout_step_scripts() {
439 $this->load_google_map_api_scripts();
440 }
441
442
443 /**
444 * Load google map places script on checkout page
445 *
446 * @since 3.1.3
447 */
448 public function load_google_map_api_scripts() {
449 $google_map_api_key = Wpfnl_functions::get_google_map_api_key();
450 if ( !$google_map_api_key ) {
451 return;
452 }
453
454 if( !Wpfnl_functions::validate_google_places_api_key($google_map_api_key) ){
455 return;
456 }
457
458 wp_enqueue_script(
459 'wpfnl-google-places-api',
460 'https://maps.googleapis.com/maps/api/js?key=' . $google_map_api_key . '&libraries=places',
461 array(),
462 WPFNL_VERSION,
463 true
464 );
465
466 wp_enqueue_script(
467 'wpfnl-google-places',
468 WPFNL_DIR_URL.'public/assets/js/google-places-auto-complete.js',
469 array( 'wpfnl-google-places-api' ),
470 WPFNL_VERSION,
471 true
472 );
473
474 }
475
476
477 /**
478 * Enable checkout hook
479 */
480 private function enable_checkout_hook(){
481
482 if ( Wpfnl_functions::check_if_this_is_step_type('checkout') ) {
483
484 if( is_plugin_active( 'woocommerce-german-market/WooCommerce-German-Market.php' )){
485 add_action( 'woocommerce_checkout_order_review','woocommerce_checkout_payment' );
486 }
487 }
488 }
489
490
491 /**
492 * Save checkout metas
493 *
494 * @param $order_id
495 * @param $posted
496 *
497 * @since 1.0.0
498 */
499 public function save_checkout_fields( $order_id, $posted )
500 {
501 // Instantiate the CheckoutModuleHelper
502 $helper = CheckoutHelper::getInstance();
503
504 if (isset($_POST['_wpfunnels_checkout_id'])) {
505 $helper->update_checkout_id_post_meta($order_id, sanitize_text_field( $_POST['_wpfunnels_checkout_id'] ) );
506 if (isset($_POST['_wpfunnels_funnel_id'])) {
507 $funnel_id = $helper->update_order_meta_for_funnel($order_id, sanitize_text_field($_POST['_wpfunnels_funnel_id']));
508 }
509 if (isset($_POST['_wpfunnels_order_unique_identifier'])) {
510 $helper->update_unique_identifier_post_meta($order_id, sanitize_text_field($_POST['_wpfunnels_order_unique_identifier']));
511 }
512 $order_bump_products = get_post_meta( $_POST['_wpfunnels_checkout_id'], 'order-bump-settings' , true );
513 $helper->update_order_bump_product( $order_bump_products, $order_id );
514 }
515
516 if (isset($funnel_id)) {
517 $order = Wpfnl_functions::is_wc_active() ? wc_get_order($order_id) : null;
518
519 $funnel_id = $helper->update_order_meta_for_funnel($order_id, sanitize_text_field( $_POST['_wpfunnels_funnel_id']) );
520
521 if( $funnel_id ){
522 $discount_instance = new WpfnlDiscount();
523
524 if( !$discount_instance->maybe_time_bound_discount( $_POST['_wpfunnels_checkout_id'] ) || ($discount_instance->maybe_time_bound_discount( $_POST['_wpfunnels_checkout_id'] ) && $discount_instance->maybe_validate_discount_time( $_POST['_wpfunnels_checkout_id'] )) ){
525 $discount = $discount_instance->get_discount_settings( $_POST['_wpfunnels_checkout_id'] );
526 $main_products = $helper->get_main_products( $_POST['_wpfunnels_checkout_id'], $funnel_id );
527
528 $variable_product_check = $helper->variable_product_check( $order->get_items(), $main_products, false );
529 $main_products = isset( $variable_product_check['main_products'] ) ? $variable_product_check['main_products'] : $main_products;
530
531 $is_main_product_in_cart = isset( $variable_product_check['is_main_product_in_cart'] ) ? $variable_product_check['is_main_product_in_cart'] : false;
532 $is_regular = isset( $discount['discountOptions'], $discount['discountapplyto']) && 'original' !== $discount['discountOptions'] && 'sale' !== $discount['discountapplyto'];
533
534 $total = $helper->get_custom_subtotal($is_regular);
535 $response = $helper->apply_discount_and_update_total( $order, $total, $discount, $is_main_product_in_cart);
536 }
537 }
538 $user_id = $order->get_user_id();
539 $orders[sanitize_text_field($_POST['_wpfunnels_checkout_id'])] = $order_id;
540 WC()->session->set('wpfnl_orders_'.$user_id.'_'.$funnel_id, $orders);
541
542 $session_handler = new WC_Session_Handler();
543 WC()->session->set('wpfnl_order_owner', $session_handler->generate_customer_id());
544
545 /**
546 * Fires after a funnel order is placed.
547 *
548 * @since 1.0.0
549 *
550 * @param int $order_id The ID of the order.
551 * @param string $funnel_id The ID of the funnel.
552 * @param string $_POST['_wpfunnels_checkout_id'] The checkout ID associated with the order.
553 */
554 do_action( 'wpfunnels/funnel_order_placed', $order_id, $funnel_id, $_POST['_wpfunnels_checkout_id'] );
555 }
556 }
557
558
559 /**
560 * Automatically add products to funnel checkout
561 *
562 * @throws \Exception
563 *
564 * @since 1.0.0
565 */
566 public function initialize_cart_data() {
567 global $post;
568 if ( isset($_REQUEST['wc-ajax']) && ( 'apply_coupon' === $_REQUEST['wc-ajax'] || 'wc_stripe_get_cart_details' === $_REQUEST['wc-ajax'] ) ) {
569 return;
570 }
571
572 if(
573 is_admin()
574 || isset( $_GET[ 'removed_item' ] )
575 || !$post
576 ) {
577 return;
578 }
579
580 if(!Wpfnl_functions::is_wc_active() ){
581 return;
582 }
583
584 $checkout_id = '';
585 if( wp_doing_ajax() ){
586 $checkout_id = Wpfnl_functions::get_checkout_id_from_post($_POST);
587 }
588
589 $checkout_id = !$checkout_id ? $post->ID : $checkout_id;
590
591 // Returns if the post id is not funnel checkout type.
592 if(
593 !Wpfnl_functions::check_if_this_is_step_type_by_id( $checkout_id, 'checkout' )
594 || !Wpfnl_functions::check_if_this_is_step_type( 'checkout' )
595 || Wpfnl_functions::is_orderbump_clicked_from_post_data()
596 || Wpfnl_functions::is_checkoutify_orderbump_clicked_from_post_data()
597 || Wpfnl_functions::is_product_option_from_post_data()
598 || Wpfnl_functions::is_variation_selected_from_post_data()
599 || Wpfnl_functions::maybe_select_quantity_from_post_data()
600 // || Wpfnl_functions::is_coupon_applied_from_post_data()
601 ) {
602 return;
603 }
604
605
606 if( isset($_REQUEST['wc-ajax']) && Wpfnl_functions::is_wc_active() && !WC()->cart->is_empty() && WC()->cart->has_discount()){
607 return;
608 }
609
610 $funnel_id = get_post_meta( $checkout_id, '_funnel_id', true );
611
612 // Returns in case of invalid funnel id or if the funnel id is `lms` type.
613 if( !$funnel_id || 'lms' === get_post_meta( $funnel_id, '_wpfnl_funnel_type', true ) ) {
614 return;
615 }
616
617 $this->render_express_checkout( $checkout_id );
618
619 /**
620 * Filter hook to modify funnel products
621 *
622 * @param array $product_array Products selected for funnel checkout.
623 * @param string|int $funnel_id Funnel ID.
624 * @param string|int $checkout_id Checkout ID.
625 *
626 * @since 2.7.11
627 */
628 $product_array = apply_filters(
629 'wpfunnels/funnel_products',
630 get_post_meta( $checkout_id, '_wpfnl_checkout_products', true ),
631 $funnel_id,
632 $checkout_id
633 );
634
635 /**
636 * This line of code applies a filter to determine if initiating a funnel checkout is allowed.
637 *
638 * @param bool $is_allow The current value indicating if initiating a funnel checkout is allowed.
639 * @param int $funnel_id The ID of the funnel being checked.
640 * @return bool The filtered value indicating if initiating a funnel checkout is allowed.
641 *
642 * @since 3.3.2
643 */
644 $is_allow = apply_filters('wpfunnels/allow_initiate_funnel_checkout', true, $funnel_id );
645 if( !$is_allow ){
646 return;
647 }
648
649 $is_store_checkout = 'store_checkout' === get_post_meta( $funnel_id, '_wpfnl_funnel_type', true );
650
651 if( !$is_store_checkout ) {
652 \WC()->cart->empty_cart();
653 }
654
655 if( !$is_store_checkout && ( !is_array( $product_array ) || empty( $product_array[ 0 ][ 'id' ] ) )) {
656 wc_clear_notices();
657 wc_add_notice( __( 'No product is added to the funnel. Please add product from checkout step settings.', 'wpfnl' ), 'error' );
658 return;
659 }
660 $price_type = $this->get_product_price_type( $funnel_id, $checkout_id );
661
662 if( $product_array && is_array( $product_array )) {
663 foreach( $product_array as $product ) {
664 if( isset( $product[ "id" ] ) && isset( $product[ "quantity" ] ) && ( !isset($product['funnel_id']) || (isset($product['funnel_id']) && (int)$funnel_id === (int)$product['funnel_id']) ) ) {
665 $product_id = !empty($product['variation_id']) ? $product[ "variation_id" ] : $product[ "id" ];
666 $quantity = $product[ "quantity" ];
667 $_product = $product_id ? wc_get_product( $product_id ) : null;
668
669 if( $_product instanceof \WC_Product ) {
670 //check if product type is variable or not from $_product object.
671
672 if( 'variable' === $_product->get_type() || 'variable-subscription' === $_product->get_type() ) {
673 $this->add_default_variation( $product_id, $_product, $quantity, $price_type );
674 continue;
675 }
676
677 $product_type = $_product->get_type();
678
679 if( 'variation' === $product_type || 'subscription_variation' === $product_type ) {
680 $is_perfect_variation = Wpfnl_functions::is_perfect_variations( $product_id );
681 $type = empty($is_perfect_variation['status']) ? 'single-variation' : 'variation';
682 $this->add_variaiton_product_in_cart( $product_id, $quantity, $price_type, $type, [], null );
683 continue;
684 }
685
686 $product_object = wc_get_product( $product_id );
687
688 if ( is_a( $product_object, 'WC_Product_Bundle' ) ) {
689
690 if( $price_type == 'original' || $price_type == 'sale' ){
691 $custom_data = [
692 'custom_price' => $product_object->get_bundle_price('min') || 0 == $product_object->get_bundle_price('min') ? $product_object->get_bundle_price('min') : $product_object->get_bundle_regular_price( 'min' ),
693 'product_type' => $product_type
694 ];
695 }else{
696 $custom_data = [
697 'custom_price' => $product_object->get_bundle_regular_price( 'min' ) ? $product_object->get_bundle_regular_price( 'min' ) : $product_object->get_bundle_price('min'),
698 'product_type' => $product_type
699 ];
700 }
701 $is_bundle_exist = true;
702
703 }else{
704 $is_bundle_exist = false;
705 if( $price_type == 'original' || $price_type == 'sale' ){
706 $custom_data = [
707 'custom_price' => $product_object->get_sale_price() ? $product_object->get_sale_price() : get_post_meta($product_id, '_price', true),
708 'product_type' => $product_type
709 ];
710 }else{
711 $custom_data = [
712 'custom_price' => get_post_meta($product_id, '_regular_price', true) ? get_post_meta($product_id, '_regular_price', true) : get_post_meta($product_id, '_price', true),
713 'product_type' => $product_type
714 ];
715 }
716
717 }
718
719
720
721 /**
722 * Apply filters to modify the product price data.
723 *
724 * @param float $custom_price The custom price of the product.
725 * @return float The modified custom price.
726 *
727 * @since 3.1.2
728 */
729
730 $custom_data['custom_price'] = apply_filters( 'wpfunnels/modify_main_product_price_data', $custom_data['custom_price'] );
731 if( (wp_doing_ajax() && $is_bundle_exist) ){
732 $custom_data = [];
733 }
734 if( 'composite' === $product_object->get_type() ){
735 $is_regular = $price_type == 'original' || $price_type == 'sale' ? false : true;
736 Wpfnl_functions::add_composite_product_to_cart_with_defaults( $product_id, $quantity,$is_regular );
737 }else{
738 WC()->cart->add_to_cart( $product_id, $quantity, '' , array(), $custom_data );
739 }
740
741 }
742 }
743 }
744 }
745
746 //Add coupons for funnel checkout.
747 $this->add_coupons( $checkout_id );
748
749 /**
750 * After initialize funnels checkout
751 *
752 * This hook will trigger in checkout page
753 *
754 * @param init $funnel_id Funnel ID
755 *
756 * @since 2.7.19
757 */
758 do_action( 'wpfunnels/after_init_checkout', $funnel_id );
759 }
760
761
762 public function add_bundle_product_in_cart( $product_id,$quantity, $custom_data , $bundled_items_data ){
763 /**
764 * Apply filters to modify the product price data.
765 *
766 * @param float $custom_price The custom price of the product.
767 * @return float The modified custom price.
768 *
769 * @since 3.1.2
770 */
771 $custom_data['custom_price'] = apply_filters( 'wpfunnels/modify_main_product_price_data', $custom_data['custom_price'] );
772
773
774 WC()->cart->add_to_cart( $product_id, $quantity, '' , array(), $custom_data, $bundled_items_data );
775 }
776
777
778 /**
779 * Checks if a variation is incomplete based on its attributes.
780 *
781 * @param int $variation_id The ID of the variation.
782 * @param \WC_Product $parent_product (Optional) The parent product object. If not provided, it will be retrieved based on the variation ID.
783 *
784 * @return bool True if the variation is incomplete, false otherwise.
785 */
786 private function is_variation_incomplete( $variation_id, $parent_product = null ) {
787 if( !$parent_product ) {
788 $parent_product = wc_get_product( $variation_id );
789 }
790
791 $var_product = wc_get_product( $variation_id );
792 $attributes = $parent_product->get_attributes();
793 $var_attributes = $var_product->get_attributes();
794
795 return sizeof( $attributes ) !== sizeof( array_filter( $var_attributes ) );
796 }
797
798
799 /**
800 * Adds coupons to the funnel checkout
801 *
802 * @param int|string $checkout_id Funnel checkout ID.
803 *
804 * @since 2.7.11
805 */
806 private function add_coupons( $checkout_id ) {
807 $coupon = get_post_meta( $checkout_id, '_wpfnl_checkout_discount', true );
808 if( is_array( $coupon ) && !empty( $coupon ) ) {
809 $coupon = reset( $coupon );
810 }
811 WC()->session->set( 'order_bump_accepted', 'no' );
812
813 if( !empty( $coupon ) ) {
814 WC()->cart->add_discount( $coupon );
815 }
816 }
817
818 /**
819 * Renders express funnel checkout
820 *
821 * @param int|string $checkout_id Funnel checkout ID.
822 *
823 * @since 2.7.11
824 */
825 private function render_express_checkout( $checkout_id ) {
826 // Only for express checkout
827 if( \WPFunnels\Wpfnl_functions::is_wpfnl_pro_activated() && Wpfnl_functions::maybe_express_checkout( $checkout_id ) ) {
828 remove_action( 'woocommerce_checkout_order_review', 'woocommerce_order_review' );
829 add_filter( 'woocommerce_update_order_review_fragments', [ $this, 'wpfnl_express_checkout_shipping_table_update' ], 10, 1 );
830 }
831 }
832
833 /**
834 * Get the first id of available product variations by parent id
835 *
836 * @param int|string $parent_id Parent product id.
837 *
838 * @return int|string|null
839 * @since 2.7.11
840 */
841 private function get_first_variation_id( $parent_id ) {
842 $product_id = get_posts( [
843 'post_parent' => $parent_id,
844 'fields' => 'ids',
845 'post_type' => 'product_variation',
846 'post_status' => [ 'private', 'publish' ],
847 'numberposts' => 1,
848 'orderby' => 'menu_order',
849 'order' => 'asc'
850 ] );
851 return !empty( $product_id[ 0 ] ) ? $product_id[ 0 ] : null;
852 }
853 /**
854 * Add default variation to funnel checkout
855 *
856 * @param string|int $product_id Variable parent ID.
857 * @param \WC_Product $product Variable parent product.
858 * @param string|int $quantity Product quantity.
859 *
860 * @return void
861 * @since 2.7.11
862 * @throws \Exception
863 */
864 private function add_default_variation( $product_id, \WC_Product $product, $quantity, $price_type ) {
865 $attributes = $product->get_attributes();
866 $variation_id = null;
867 $default_variation = $product->get_default_attributes();
868 $formatted_variation = [];
869
870 if( sizeof( $attributes ) === sizeof( $default_variation ) ) {
871 $formatted_variation = $this->get_formatted_variation( $default_variation );
872 if( !empty( $formatted_variation ) ) {
873 $variation_id = ( new \WC_Product_Data_Store_CPT() )->find_matching_product_variation(
874 $product,
875 $formatted_variation
876 );
877 }
878 }
879 else {
880 $variation_id = $this->get_first_variation_id( $product_id );
881 }
882
883 if( $variation_id ) {
884 $this->add_variaiton_product_in_cart( $variation_id, $quantity, $price_type, 'variable', $formatted_variation, $product);
885 }
886 }
887
888 /**
889 * Adds a variation product to the cart.
890 *
891 * @param int $variation_id The ID of the variation.
892 * @param int $quantity The quantity of the product to add to the cart.
893 * @param array $formatted_variation The formatted variation array with attribute keys and values.
894 * @param \WC_Product $parent_product (Optional) The parent product object. If not provided, it will be retrieved based on the variation ID.
895 *
896 * @return void
897 * @since 2.7.11
898 */
899 private function add_variaiton_product_in_cart( $variation_id, $quantity, $price_type, $product_type, $formatted_variation = [], $parent_product = null ) {
900 if( !$quantity || !$variation_id ) {
901 return;
902 }
903
904 $parent_id = wc_get_product( $variation_id )->get_parent_id();
905 if( !$parent_product ) {
906 $parent_product = wc_get_product( $parent_id );
907 }
908
909 if( empty( $formatted_variation ) ) {
910 $is_var_incomplete = $this->is_variation_incomplete( $variation_id, $parent_product );
911 $formatted_variation = $is_var_incomplete ? $this->get_complete_variation_format( $variation_id, $parent_product ) : $formatted_variation;
912 }
913 $custom_data = [];
914 if( $price_type == 'original' || $price_type == 'sale' ){
915 $custom_data = [
916 'custom_price' => get_post_meta($variation_id, '_price', true) ? get_post_meta($variation_id, '_price', true) : get_post_meta($variation_id, '_regular_price', true),
917 'product_type' => $product_type
918 ];
919 }else{
920 $custom_data = [
921 'custom_price' => get_post_meta($variation_id, '_regular_price', true) ? get_post_meta($variation_id, '_regular_price', true) : get_post_meta($variation_id, '_price', true),
922 'product_type' => $product_type
923 ];
924
925 }
926
927 /**
928 * Apply filters to modify the product price data.
929 *
930 * @param float $custom_price The custom price of the product.
931 * @return float The modified custom price.
932 *
933 * @since 3.1.2
934 */
935 $custom_data['custom_price'] = apply_filters( 'wpfunnels/modify_main_product_price_data', $custom_data['custom_price'] );
936
937 WC()->cart->add_to_cart( $parent_id, $quantity, $variation_id, $formatted_variation, $custom_data );
938 }
939
940
941
942 /**
943 * Formats the variation attributes into a formatted variation array.
944 *
945 * @param array $var_attributes The variation attributes.
946 *
947 * @return array The formatted variation array with attribute keys and values.
948 * @since 2.7.11
949 */
950 private function get_formatted_variation( array $var_attributes ) {
951 $formatted_variation = [];
952
953 foreach( $var_attributes as $key => $value ) {
954 $formatted_variation[ "attribute_{$key}" ] = $value;
955 }
956 return $formatted_variation;
957 }
958
959 /**
960 * Retrieves the complete variation format for a given variation ID and parent product.
961 *
962 * @param int $variation_id The variation ID.
963 * @param \WC_Product $parent_product (Optional) The parent product object. If not provided, it will be retrieved based on the variation ID.
964 *
965 * @return array The formatted variation array with attribute keys and values.
966 * @since 2.7.11
967 */
968 private function get_complete_variation_format( $variation_id, $parent_product = null ) {
969 if( !$variation_id ) {
970 return [];
971 }
972 if( !$parent_product ) {
973 $parent_product = wc_get_product( $variation_id );
974 }
975 $var_product = wc_get_product( $variation_id );
976 $var_attributes = $var_product instanceof \WC_Product ? $var_product->get_attributes() : [];
977 $formatted_variation = [];
978
979 foreach( $var_attributes as $key => $value ) {
980 $value = $value ?: strtolower( $this->get_first_attribute_value( $key, $parent_product ) );
981 $formatted_variation[ "attribute_{$key}" ] = $value;
982 }
983 return $formatted_variation;
984 }
985
986 /**
987 * Retrieves the first value of a specified attribute from a WooCommerce product.
988 *
989 * @param string $key The attribute key.
990 * @param \WC_Product $product The WooCommerce product object.
991 *
992 * @return string The first attribute value, or an empty string if not found.
993 * @since 2.7.11
994 */
995 private function get_first_attribute_value( $key, \WC_Product $product ) {
996 $attribute = $product->get_attribute( $key );
997 if( strpos( $attribute, '|' ) ) {
998 $attribute = explode( '|', $attribute );
999 }
1000 else {
1001 $attribute = explode( ',', $attribute );
1002 }
1003 return !empty( $attribute[ 0 ] ) ? trim( $attribute[ 0 ] ) : '';
1004 }
1005
1006 /*============*/
1007
1008 /**
1009 * Update shiiping markup for express checkout
1010 *
1011 * @since 2.4.18
1012 */
1013 public function wpfnl_express_checkout_shipping_table_update( $fragments ) {
1014 ob_start();
1015 require WPFNL_DIR.'/woocommerce/templates/checkout/review-order.php';
1016 $woocommerce_shipping_methods = ob_get_clean();
1017 $fragments['.woocommerce-checkout-review-order-table'] = $woocommerce_shipping_methods;
1018 return $fragments;
1019 }
1020
1021 /**
1022 * Render content for wpfunnels_checkout shortcode
1023 *
1024 * @param $atts
1025 *
1026 * @return string
1027 *
1028 * @since 2.0.3
1029 */
1030 public function render_checkout_shortcode($atts)
1031 {
1032 $atts = shortcode_atts(
1033 array(
1034 'id' => 0,
1035 ),
1036 $atts
1037 );
1038 $checkout_id = intval($atts['id']);
1039 if (is_admin()) {
1040 if (0 === $checkout_id && isset($_POST['id'])) {
1041 $checkout_id = intval($_POST['id']);
1042 }
1043 }
1044
1045 if (empty($checkout_id)) {
1046 global $post;
1047 if(is_object($post)) {
1048 $checkout_id = intval($post->ID);
1049 }
1050 }
1051 if(!$checkout_id) {
1052 return '';
1053 }
1054 $checkout_layout = Wpfnl::get_instance()->meta->get_checkout_meta_value($checkout_id, 'wpfnl_checkout_layout', 'wpfnl-col-2');
1055
1056 $floating_label = Wpfnl::get_instance()->meta->get_checkout_meta_value($checkout_id, 'wpfnl_floating_label', '');
1057
1058 $output = '';
1059 ob_start();
1060
1061 do_action('wpfunnels/before_gb_checkout_form', $checkout_id);
1062
1063 $template_file = WPFNL_DIR . "public/modules/checkout/templates/checkout-template.php";
1064 include $template_file;
1065
1066 do_action('wpfunnels/after_checkout_form', $checkout_id);
1067
1068 $output .= ob_get_clean();
1069 return $output;
1070
1071 }
1072
1073
1074 public function custom_price_to_cart_item($cart)
1075 {
1076 if ( wp_doing_ajax() && !WC()->session->__isset('reload_checkout')) {
1077 foreach ($cart->cart_contents as $key => $value) {
1078 if (isset($value['custom_price'])) {
1079 $custom_price = floatval($value['custom_price']);
1080 $value['data']->set_price($custom_price);
1081 }
1082 }
1083 }
1084 }
1085
1086 /**
1087 * Auto apply coupon on funnel checkout
1088 *
1089 * @param object $cart_object
1090 *
1091 * @since 2.7.9
1092 * @return void
1093 */
1094 public function apply_auto_coupon($cart_object)
1095 {
1096 global $post;
1097 if( !$post ){
1098 return false;
1099 }
1100 $checkout_id = '';
1101 if ( wp_doing_ajax() || isset( $_GET['wc-ajax'] ) ) {
1102 $checkout_id = Wpfnl_functions::get_checkout_id_from_post($_POST);
1103 } else {
1104 $checkout_id = $post->ID;
1105 }
1106 if( $checkout_id ){
1107 $coupon = Wpfnl_functions::get_applied_coupon( $checkout_id );
1108 if( $coupon ){
1109 if ( WC()->cart->has_discount( $coupon ) ) return;
1110 WC()->cart->apply_coupon( $coupon );
1111 add_action( 'woocommerce_after_order_notes', [$this, 'add_coupon_field']);
1112 wc_print_notices();
1113 }
1114 }
1115
1116 }
1117
1118 public function render_order_bump_popup()
1119 {
1120 $step_id = get_the_ID();
1121 $order_bump = get_post_meta($step_id, 'order-bump', true);
1122 $order_bump_settings = get_post_meta($step_id, 'order-bump-settings', true);
1123 require_once WPFNL_DIR . 'public/modules/checkout/templates-style/order-bump-template-style3.php';
1124 }
1125
1126
1127
1128 /**
1129 * Insert helper hidden ids
1130 *
1131 * @since 1.0.0
1132 */
1133 public function checkout_shortcode_metas()
1134 {
1135 if (Wpfnl_functions::check_if_this_is_step_type('checkout')) {
1136 global $post;
1137 $this->step_id = $post->ID;
1138 $step = new Wpfnl_Steps_Store_Data();
1139 $step->read($this->step_id);
1140 $this->funnel_id = $step->get_funnel_id();
1141 echo '<input type="hidden" class="input-hidden _wpfunnels_funnel_id" name="_wpfunnels_funnel_id" value="' . intval($this->funnel_id) . '">';
1142 echo '<input type="hidden" class="input-hidden _wpfunnels_checkout_id" name="_wpfunnels_checkout_id" value="' . intval($post->ID) . '">';
1143 echo '<input type="hidden" class="input-hidden _wpfunnels_order_unique_identifier" name="_wpfunnels_order_unique_identifier" value="' . uniqid() . '">';
1144 echo '<input type="hidden" class="input-hidden _wpfunnels_select_quantity" name="_wpfunnels_select_quantity" value="no">';
1145 }
1146 }
1147
1148
1149 /**
1150 * Insert helper hidden ids
1151 *
1152 * @since 1.0.0
1153 */
1154 public function add_coupon_field()
1155 {
1156 if (Wpfnl_functions::check_if_this_is_step_type('checkout')) {
1157 echo '<input type="hidden" class="input-hidden _wpfunnels_funnel_id" name="_wpfunnels_auto_coupon" value="yes">';
1158 }
1159 }
1160
1161
1162 /**
1163 * Next step button ajax
1164 *
1165 * @since 1.0.0
1166 */
1167 public function wpfnl_next_button_ajax()
1168 {
1169
1170 $step_id = sanitize_text_field($_POST['step_id']);
1171 $funnel_id = get_post_meta($step_id, '_funnel_id', true);
1172 $parent_step_id = get_post_meta($step_id, '_parent_step_id', true);
1173 $step_id= $parent_step_id ? $parent_step_id : $step_id;
1174 $next_step = Wpfnl_functions::get_next_conditional_step( $funnel_id, $step_id );
1175 $url = isset($_POST['url']) ? $_POST['url'] : '';
1176 $parts = parse_url($url);
1177 $query = [];
1178 if( isset($parts['query']) ){
1179 parse_str($parts['query'], $query);
1180 }
1181
1182 $next_step = apply_filters( 'wpfunnels/next_step_data', $next_step );
1183
1184 if ( $next_step ) {
1185
1186 $custom_url = Wpfnl_functions::custom_url_for_thankyou_page( $next_step['step_id'] );
1187 if( $custom_url ){
1188 $response = $custom_url;
1189 }else{
1190 $redirect_url = add_query_arg( $this->prepare_query_param($query), get_the_permalink($next_step['step_id']));
1191 if ($redirect_url) {
1192 $response = $redirect_url;
1193 } else {
1194 $response = 'error';
1195 }
1196 }
1197
1198 ob_start();
1199 do_action( 'wpfunnels/trigger_cta', $step_id, $funnel_id );
1200 ob_get_clean();
1201
1202 wp_send_json($response);
1203 }
1204 return false;
1205 }
1206
1207
1208 /**
1209 * Check if an email exists and return CartFlows-compatible response shape.
1210 *
1211 * @return void
1212 */
1213 public function wpfnl_check_email_exists() {
1214
1215 if ( isset( $_POST['security'] ) && ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['security'] ) ), 'wpfnl' ) ) {
1216 wp_send_json_error(
1217 array(
1218 'success' => false,
1219 'is_login_allowed' => false,
1220 'msg' => __( 'Invalid request.', 'wpfnl' ),
1221 )
1222 );
1223 }
1224
1225 $email = isset( $_POST['email'] ) ? sanitize_email( wp_unslash( $_POST['email'] ) ) : '';
1226
1227 if ( empty( $email ) || ! is_email( $email ) ) {
1228 wp_send_json_success(
1229 array(
1230 'success' => false,
1231 'is_login_allowed' => 'yes' === get_option( 'woocommerce_enable_checkout_login_reminder' ),
1232 'msg' => __( 'Invalid email address.', 'wpfnl' ),
1233 )
1234 );
1235 }
1236
1237 $email_exists = (bool) email_exists( $email );
1238 $is_login_allowed = 'yes' === get_option( 'woocommerce_enable_checkout_login_reminder' );
1239
1240 wp_send_json_success(
1241 array(
1242 'success' => $email_exists,
1243 'is_login_allowed' => $is_login_allowed,
1244 'msg' => $email_exists ? __( 'Email exists', 'wpfnl' ) : __( 'Email not exist', 'wpfnl' ),
1245 )
1246 );
1247 }
1248
1249
1250 /**
1251 * Prepare query param for redirect url
1252 *
1253 * @param String $name
1254 * @param Array $query
1255 *
1256 * @return Array
1257 * @since 2.5.9
1258 */
1259 private function prepare_query_param( $query ){
1260 $query_param = [];
1261 if( isset( $query['optin'], $query['uname'] ) ){
1262 $query_param['optin'] = $query['optin'];
1263 $query_param['uname'] = $query['uname'];
1264 }
1265
1266 if( isset($query['wpfnl-order'], $query['wpfnl-key']) ){
1267 $query_param['wpfnl-order'] = $query['wpfnl-order'];
1268 $query_param['wpfnl-key'] = $query['wpfnl-key'];
1269 }
1270
1271 /**
1272 * Filter to change the query parameter of the redirect URL of funnel's steps.
1273 * Returns the query parameter after applying the 'wpfunnels/prepare_query_param' filter.
1274 *
1275 * @param array $query_param
1276 * @return array $query_param
1277 *
1278 * @since 3.4.13
1279 */
1280 return apply_filters( 'wpfunnels/prepare_query_param', $query_param );
1281 }
1282
1283
1284 /**
1285 * Calculate Discount.
1286 *
1287 * @since 1.1.5
1288 */
1289 public function calculate_custom_price($discount_type, $discount_value, $product_price)
1290 {
1291 $custom_price = $product_price;
1292
1293 if (!empty($discount_type)) {
1294 if ('discount-percentage' === $discount_type) {
1295 if ( $discount_value > 0 && $discount_value <= 100) {
1296 $custom_price = $product_price - (($product_price * $discount_value) / 100);
1297 }else{
1298 $custom_price = $product_price;
1299 }
1300 } elseif ('discount-price' === $discount_type) {
1301 if ($discount_value > 0 && $product_price >= $discount_value ) {
1302 $custom_price = $product_price - $discount_value;
1303 }else{
1304 $custom_price = $product_price;
1305 }
1306 }
1307 }
1308
1309 return number_format($custom_price, 2);
1310 }
1311
1312 public function wpfnl_update_variation(){
1313
1314 $variations = $_POST['variations'];
1315 $product_id = $_POST['variations'][0]['product_id'];
1316 $variation_id = $_POST['variations'][0]['variation_id'];
1317 $quantity = $_POST['variations'][0]['quantity'];
1318
1319 $_product = wc_get_product($variation_id);
1320 if( $_product ){
1321 $attribute = $_product->get_attributes();
1322 foreach ( $attribute as $key => $value) {
1323 if( $value ){
1324 $formatted_variation['attribute_'.$key] = $value;
1325 }else{
1326 foreach( $variations as $variation ){
1327 if( $variation['attr'] == $key ){
1328 $formatted_variation['attribute_'.$key] = $variation['value'];
1329 }
1330 }
1331 }
1332 }
1333 }
1334
1335
1336 $variation_id = (new \WC_Product_Data_Store_CPT())->find_matching_product_variation(
1337 new \WC_Product($product_id),
1338 $formatted_variation
1339 );
1340
1341 foreach ( WC()->cart->get_cart() as $cart_item_key => $cart_item ) {
1342 if( !isset($cart_item['product_type'])|| (isset($cart_item['product_type']) && 'single-variation' === $cart_item['product_type']) ){
1343 if ($cart_item['product_id'] == $product_id) {
1344 WC()->cart->remove_cart_item($cart_item_key);
1345 }
1346 }
1347 }
1348 $cart_item_data = [
1349 'custom_price' => get_post_meta($variation_id, '_price', true) ? get_post_meta($variation_id, '_price', true) : get_post_meta($variation_id, '_regular_price', true),
1350 'product_type' => 'single-variation'
1351 ];
1352
1353 WC()->cart->add_to_cart( $product_id, $quantity, $variation_id, $formatted_variation, $cart_item_data);
1354 }
1355
1356
1357
1358
1359
1360 /**
1361 * Order Bump Ajax Initialized
1362 * When you click on add order bump button it will add the product to cart and click again to recover cart
1363 *
1364 * @since 1.1.
1365 */
1366 public function wpfnl_get_course_details()
1367 {
1368 $course_id = filter_input(INPUT_POST, 'course_id', FILTER_VALIDATE_INT);
1369 $course_meta = Wpfnl_lms_learndash_functions::get_course_details_by_id($course_id);
1370 // ld_update_course_access( 1, $course_id, false );
1371 $data = [
1372 'status' => 'success',
1373 'course' => $course_meta,
1374 ];
1375
1376 wp_send_json( $data );
1377
1378 }
1379
1380
1381 /**
1382 * Order Bump Ajax Initialized
1383 * When you click on add order bump button it will add the product to cart and click again to recover cart
1384 *
1385 * @since 1.1.
1386 */
1387 public function wpfnl_order_bump_ajax() {
1388 $step_id = filter_input( INPUT_POST, 'step_id', FILTER_VALIDATE_INT );
1389 $product_id = filter_input( INPUT_POST, 'product', FILTER_VALIDATE_INT );
1390 $variation_id = filter_input( INPUT_POST, 'variation_id', FILTER_VALIDATE_INT );
1391 $checker = filter_input( INPUT_POST, 'checker', FILTER_SANITIZE_SPECIAL_CHARS );
1392 $quantity = filter_input( INPUT_POST, 'quantity', FILTER_SANITIZE_SPECIAL_CHARS );
1393 $key = filter_input( INPUT_POST, 'key', FILTER_SANITIZE_SPECIAL_CHARS );
1394 $is_lms = filter_input( INPUT_POST, 'is_lms', FILTER_SANITIZE_SPECIAL_CHARS );
1395 $user_id = filter_input( INPUT_POST, 'user_id', FILTER_SANITIZE_SPECIAL_CHARS );
1396 $funnel_id = Wpfnl_functions::get_funnel_id_from_step( $step_id );
1397 $type = get_post_meta( $funnel_id, '_wpfnl_funnel_type', true );
1398 $type = !$type ? 'wc' : $type;
1399
1400 // Validate quantity - must be a positive integer
1401 $quantity = absint( $quantity );
1402 if ( $quantity < 1 ) {
1403 $quantity = 1;
1404 }
1405
1406 // If variation_id exists, use it as product_id for cart functions.
1407 $cart_product_id = $variation_id ? $variation_id : $product_id;
1408
1409 $class_object = Wpfnl_Public_Type_Factory::build( $type );
1410 if( $class_object ) {
1411 $data = $class_object->wpfnl_order_bump_trigger( $step_id, $cart_product_id, $quantity, $key, $user_id, $funnel_id, $checker );
1412 if( $type === 'lms' ) {
1413 wp_send_json( $data );
1414 }
1415 elseif( $type === 'wc' ) {
1416 add_filter( 'woocommerce_locate_template', [ new Wpfnl_Public(), 'wpfunnels_woocommerce_locate_template' ], 20, 3 );
1417 wp_send_json( Wpfnl_functions::get_wc_fragments( $data ) );
1418 }
1419 }
1420 }
1421
1422
1423 /**
1424 * Get replaceable orderbump products
1425 */
1426 private function get_replaceable_ob_products( $ob_settings ){
1427
1428
1429 $replaceable_ob = [];
1430 foreach( $ob_settings as $key=>$settings ){
1431 if( $settings['isReplace'] === 'yes' ){
1432 $replaceable_ob[] = [
1433 'id' => $settings['product'],
1434 'quantity' => $settings['quantity'],
1435 ];
1436 }
1437 }
1438
1439 return $replaceable_ob;
1440
1441 }
1442
1443
1444 /**
1445 * Get calculable price
1446 *
1447 * @param \WC_Product $product
1448 * @param $discount_apply
1449 *
1450 * @return string
1451 *
1452 * @since 2.0.5
1453 */
1454 private function get_product_price( \WC_Product $product, $discount_apply = 'regular' ) {
1455 $price = $product->get_regular_price();
1456 // if( is_plugin_active( 'woocommerce-subscriptions/woocommerce-subscriptions.php' ) ){
1457 // $signUpFee = \WC_Subscriptions_Product::get_sign_up_fee( $product );
1458 // $price = $price + $signUpFee;
1459 // }
1460 return $discount_apply === 'sale' && $product->get_sale_price() ? $product->get_sale_price() : $price;
1461 }
1462
1463
1464 /**
1465 * Add checkout products to cart
1466 *
1467 * @param $checkout_id
1468 *
1469 * @throws \Exception
1470 */
1471 private function add_checkout_products($checkout_id)
1472 {
1473 $checkout_products = get_post_meta( $checkout_id, '_wpfnl_checkout_products', true );
1474 foreach ($checkout_products as $product) {
1475 $_product_id = $product['id'];
1476 $_qty = $product['quantity'];
1477 WC()->cart->add_to_cart($_product_id, $_qty);
1478 }
1479 }
1480
1481 /**
1482 * Add last updated checkout products to cart
1483 *
1484 * @param $updated_cart
1485 *
1486 * @throws \Exception
1487 */
1488 private function update_checkout_with_last_updated_cart($updated_cart){
1489 foreach ($updated_cart as $cart) {
1490 WC()->cart->add_to_cart( $cart['product_id'], $cart['quantity'], $cart['variation_id'], $cart['variation'], $cart['line_total']);
1491 }
1492 }
1493
1494
1495 public function wpfnl_checkout_cart()
1496 {
1497 $values = [];
1498 parse_str(sanitize_text_field($_POST['post_data']), $values);
1499 $cart = $values['cart'];
1500 foreach ($cart as $cart_key => $cart_value) {
1501 WC()->cart->set_quantity($cart_key, $cart_value['qty'], false);
1502 WC()->cart->calculate_totals();
1503 woocommerce_cart_totals();
1504 }
1505 wp_die();
1506 }
1507
1508
1509 /**
1510 * Order Bump Templates
1511 *
1512 * @param $settings
1513 *
1514 * @since 1.0.0
1515 */
1516 public function render_order_bump_template($settings)
1517 {
1518 if (!empty($settings['selectedStyle'])) {
1519 require_once WPFNL_DIR . 'public/modules/checkout/templates-style/order-bump-template-' . $settings['selectedStyle'] . '.php';
1520 }
1521 }
1522
1523
1524 /**
1525 * Display coupon field or not
1526 *
1527 * @since 2.0.5
1528 */
1529 public function display_coupon_field() {
1530 if ( Wpfnl_functions::check_if_this_is_step_type('checkout') ) {
1531 global $post;
1532 $checkout_id = $post->ID;
1533 $show_coupon = Wpfnl::get_instance()->meta->get_checkout_meta_value( $checkout_id, '_wpfnl_checkout_coupon' );
1534 if( 'no' === $show_coupon ) {
1535 remove_action( 'woocommerce_before_checkout_form', 'woocommerce_checkout_coupon_form' );
1536 }
1537 }
1538 }
1539
1540 /**
1541 * Out of Stock message in checkout page
1542 *
1543 * @param $message
1544 * @param $product_data
1545 *
1546 * @return string
1547 */
1548
1549 public function wpfnl_out_of_stock_message_in_checkout( $message, $product_data ){
1550 $message = __( 'Sorry, the following product(s) that you are willing to purchase is out of stock at the moment.', 'wpfnl' );
1551 $image = wp_get_attachment_image_src( get_post_thumbnail_id( $product_data->get_id() ), array( 100, 100) );
1552 $message .= '<div class="wpfnl-checkout-stock-message"><img src="'.$image[0].'"><h4>'.$product_data->get_title().' </h4></div>';
1553 return $message;
1554 }
1555
1556
1557 /**
1558 * Remove Astra theme checkout shipping hook
1559 */
1560 public function remove_astra_theme_checkout_shipping_hook(){
1561 return true;
1562 }
1563
1564
1565 /**
1566 * Update quantity from checkout page
1567 *
1568 * @param $quantity, $cart_item, $cart_item_key
1569 */
1570 public function wpfnl_checkout_cart_item_quantity( $quantity, $cart_item, $cart_item_key ) {
1571
1572 $step_id = 0;
1573 $isQuantity = 'no';
1574
1575 if( wp_doing_ajax() ) {
1576 $step_id = isset($_POST['step_id']) ? $_POST['step_id'] : 0;
1577 } else {
1578 $step_id = get_the_ID();
1579 }
1580
1581
1582 $isQuantity = get_post_meta($step_id, '_wpfnl_quantity_support',true);
1583 $quantityLimit = get_post_meta($step_id, '_wpfnl_quantity_limit', true);
1584 $order_bump_product = get_post_meta($step_id,'order-bump-settings',true);
1585
1586 if($isQuantity === 'yes'){
1587 if(isset($order_bump_product['product']) && isset($order_bump_product['isEnabled'])){
1588
1589 $variations = json_encode($cart_item['variation']);
1590 $product_id = $cart_item["product_id"];
1591 $quantity = $cart_item["quantity"];
1592 $variation_id = $cart_item["variation_id"];
1593 $isQuantityLimit = false;
1594 $set_quantity = 0;
1595
1596 if( isset($quantityLimit['isEnabled']) && $quantityLimit['isEnabled'] === 'yes' ){
1597 $set_quantity = $quantityLimit['quantity'];
1598 $isQuantityLimit = true;
1599 }
1600
1601 if( $isQuantityLimit ){
1602 $quantity = "× <input type='number' min='1' step='1' max='".$set_quantity."' value='".$quantity."' class='wpfnl-quantity-setect' data-product-id='".$product_id."' data-variation='".$variations."' data-variation-id='".$variation_id."' data-quantity-limit='".$set_quantity."' data-set-quantity='yes'/>";
1603 }else{
1604 $quantity = "× <input type='number' min='1' step='1' value='".$quantity."' class='wpfnl-quantity-setect' data-product-id='".$product_id."' data-variation='".$variations."' data-variation-id='".$variation_id."' data-set-quantity='no' />";
1605 } }
1606 }
1607 return $quantity;
1608
1609 }
1610
1611
1612 public function before_checkout_form_actions( $checkout_id ) {
1613
1614 if( Wpfnl_functions::is_wc_active() ){
1615
1616 remove_all_actions( 'woocommerce_checkout_billing' );
1617 remove_all_actions( 'woocommerce_checkout_shipping' );
1618
1619 add_action( 'woocommerce_checkout_billing', array( WC()->checkout, 'checkout_form_billing' ) );
1620 add_action( 'woocommerce_checkout_shipping', array( WC()->checkout, 'checkout_form_shipping' ) );
1621 }
1622
1623 }
1624
1625
1626 /**
1627 * Render custom shipping section for modern checkout only.
1628 *
1629 * @return void
1630 */
1631 public function add_custom_shipping_section() {
1632 if ( ! $this->is_modern_checkout_layout() || ! apply_filters( 'wpfunnels_should_render_custom_shipping', true ) ) {
1633 return;
1634 }
1635 ob_start();
1636 echo '<div class="wpfnl-customer-shipping">';
1637 $this->wpfnl_cart_totals_shipping_html();
1638 echo '</div>';
1639 ob_end_flush();
1640 }
1641
1642
1643 /**
1644 * Display custom coupon field in modern checkout order review section.
1645 *
1646 * @return void
1647 */
1648 public function display_custom_coupon_field() {
1649 if ( ! $this->is_modern_checkout_layout() || ! $this->is_custom_coupon_field_enabled() ) {
1650 return;
1651 }
1652
1653 $is_collapsible = $this->is_collapsible_coupon_enabled();
1654
1655 $coupon_field = apply_filters(
1656 'wpfunnels_coupon_field_options',
1657 array(
1658 'field_text' => __( 'Coupon Code', 'wpfnl' ),
1659 'button_text' => __( 'Apply', 'wpfnl' ),
1660 'class' => '',
1661 )
1662 );
1663
1664 do_action( 'wpfunnels_before_custom_coupon_field_html', $coupon_field );
1665
1666 if ( $is_collapsible ) {
1667 ?>
1668 <div class="wpfnl-coupon-toggle-wrapper">
1669 <div class="wpfnl-coupon-toggle-notice">
1670 <?php echo wp_kses_post( apply_filters( 'wpfunnels_coupon_toggle_text', __( 'Have a coupon?', 'wpfnl' ) ) ); ?>
1671 <a href="#!" class="wpfnl-coupon-toggle-link"><?php echo esc_html( apply_filters( 'wpfunnels_coupon_toggle_link_text', __( 'Click here to enter your code', 'wpfnl' ) ) ); ?></a>
1672 </div>
1673 <div class="wpfnl-custom-coupon-field wpfnl-coupon-collapsible <?php echo esc_attr( $coupon_field['class'] ); ?>" id="wpfnl_custom_coupon_field" style="display: none;">
1674 <div class="wpfnl-coupon-col-1">
1675 <span>
1676 <input type="text" name="coupon_code" class="input-text wpfnl-coupon-code-input" placeholder="<?php echo esc_attr( $coupon_field['field_text'] ); ?>" id="wpfnl_coupon_code" value="">
1677 </span>
1678 </div>
1679 <div class="wpfnl-coupon-col-2">
1680 <span>
1681 <button type="button" class="button wpfnl-submit-coupon wpfnl-btn-small" name="apply_coupon" value="Apply"><?php echo esc_html( $coupon_field['button_text'] ); ?></button>
1682 </span>
1683 </div>
1684 </div>
1685 </div>
1686 <?php
1687 } else {
1688 ?>
1689 <div class="wpfnl-custom-coupon-field <?php echo esc_attr( $coupon_field['class'] ); ?>" id="wpfnl_custom_coupon_field">
1690 <div class="wpfnl-coupon-col-1">
1691 <span>
1692 <input type="text" name="coupon_code" class="input-text wpfnl-coupon-code-input" placeholder="<?php echo esc_attr( $coupon_field['field_text'] ); ?>" id="wpfnl_coupon_code" value="">
1693 </span>
1694 </div>
1695 <div class="wpfnl-coupon-col-2">
1696 <span>
1697 <button type="button" class="button wpfnl-submit-coupon wpfnl-btn-small" name="apply_coupon" value="Apply"><?php echo esc_html( $coupon_field['button_text'] ); ?></button>
1698 </span>
1699 </div>
1700 </div>
1701 <?php
1702 }
1703 do_action( 'wpfunnels_after_custom_coupon_field_html', $coupon_field );
1704 }
1705
1706
1707 /**
1708 * Check if custom coupon field is enabled for this checkout.
1709 *
1710 * @return bool
1711 */
1712 private function is_custom_coupon_field_enabled() {
1713 if ( ! apply_filters( 'woocommerce_coupons_enabled', true ) ) {
1714 return false;
1715 }
1716
1717 $checkout_id = 0;
1718 if ( wp_doing_ajax() ) {
1719 if ( isset( $_POST['step_id'] ) ) {
1720 $checkout_id = intval( wp_unslash( $_POST['step_id'] ) );
1721 } elseif ( isset( $_POST['_wpfunnels_checkout_id'] ) ) {
1722 $checkout_id = intval( wp_unslash( $_POST['_wpfunnels_checkout_id'] ) );
1723 } elseif ( isset( $_POST['post_data'] ) ) {
1724 $post_data = array();
1725 parse_str( wp_unslash( $_POST['post_data'] ), $post_data );
1726 if ( isset( $post_data['_wpfunnels_checkout_id'] ) ) {
1727 $checkout_id = intval( $post_data['_wpfunnels_checkout_id'] );
1728 }
1729 }
1730 }
1731
1732 if ( ! $checkout_id ) {
1733 $checkout_id = get_the_ID();
1734 }
1735
1736 if ( ! $checkout_id ) {
1737 return false;
1738 }
1739
1740 $show_coupon = Wpfnl::get_instance()->meta->get_checkout_meta_value( $checkout_id, '_wpfnl_checkout_coupon' );
1741
1742 return 'yes' === $show_coupon;
1743 }
1744
1745
1746 /**
1747 * Check if collapsible coupon field is enabled for this checkout.
1748 *
1749 * @return bool
1750 */
1751 private function is_collapsible_coupon_enabled() {
1752 $checkout_id = 0;
1753 if ( wp_doing_ajax() ) {
1754 if ( isset( $_POST['step_id'] ) ) {
1755 $checkout_id = intval( wp_unslash( $_POST['step_id'] ) );
1756 } elseif ( isset( $_POST['_wpfunnels_checkout_id'] ) ) {
1757 $checkout_id = intval( wp_unslash( $_POST['_wpfunnels_checkout_id'] ) );
1758 } elseif ( isset( $_POST['post_data'] ) ) {
1759 $post_data = array();
1760 parse_str( wp_unslash( $_POST['post_data'] ), $post_data );
1761 if ( isset( $post_data['_wpfunnels_checkout_id'] ) ) {
1762 $checkout_id = intval( $post_data['_wpfunnels_checkout_id'] );
1763 }
1764 }
1765 }
1766
1767 if ( ! $checkout_id ) {
1768 $checkout_id = get_the_ID();
1769 }
1770
1771 if ( ! $checkout_id ) {
1772 return false;
1773 }
1774
1775 $collapsible = get_post_meta( $checkout_id, '_wpfnl_checkout_collapsible_coupon', true );
1776
1777 return 'yes' === $collapsible;
1778 }
1779
1780
1781 /**
1782 * Apply checkout coupon via AJAX.
1783 *
1784 * @return void
1785 */
1786 public function wpfnl_apply_checkout_coupon() {
1787 if ( isset( $_POST['security'] ) && ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['security'] ) ), 'wpfnl' ) ) {
1788 wp_send_json_error( array( 'messages' => '<ul class="woocommerce-error" role="alert"><li>' . esc_html__( 'Invalid request.', 'wpfnl' ) . '</li></ul>' ) );
1789 }
1790
1791 $coupon_code = isset( $_POST['coupon_code'] ) ? wc_format_coupon_code( sanitize_text_field( wp_unslash( $_POST['coupon_code'] ) ) ) : '';
1792 if ( empty( $coupon_code ) ) {
1793 wp_send_json_error( array( 'messages' => '<ul class="woocommerce-error" role="alert"><li>' . esc_html__( 'Please enter a coupon code.', 'wpfnl' ) . '</li></ul>' ) );
1794 }
1795
1796 if ( ! WC()->cart ) {
1797 wp_send_json_error( array( 'messages' => '<ul class="woocommerce-error" role="alert"><li>' . esc_html__( 'Cart is not available.', 'wpfnl' ) . '</li></ul>' ) );
1798 }
1799
1800 $applied = WC()->cart->apply_coupon( $coupon_code );
1801 WC()->cart->calculate_totals();
1802
1803 ob_start();
1804 wc_print_notices();
1805 $messages = ob_get_clean();
1806
1807 if ( $applied ) {
1808 wp_send_json_success( array( 'messages' => $messages ) );
1809 }
1810
1811 wp_send_json_error( array( 'messages' => $messages ) );
1812 }
1813
1814
1815 /**
1816 * Remove checkout coupon via AJAX.
1817 *
1818 * @return void
1819 */
1820 public function wpfnl_remove_checkout_coupon() {
1821 if ( isset( $_POST['security'] ) && ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['security'] ) ), 'wpfnl' ) ) {
1822 wp_send_json_error( array( 'messages' => '<ul class="woocommerce-error" role="alert"><li>' . esc_html__( 'Invalid request.', 'wpfnl' ) . '</li></ul>' ) );
1823 }
1824
1825 $coupon_code = isset( $_POST['coupon_code'] ) ? wc_format_coupon_code( sanitize_text_field( wp_unslash( $_POST['coupon_code'] ) ) ) : '';
1826 if ( empty( $coupon_code ) ) {
1827 wp_send_json_error( array( 'messages' => '<ul class="woocommerce-error" role="alert"><li>' . esc_html__( 'Invalid coupon code.', 'wpfnl' ) . '</li></ul>' ) );
1828 }
1829
1830 if ( ! WC()->cart ) {
1831 wp_send_json_error( array( 'messages' => '<ul class="woocommerce-error" role="alert"><li>' . esc_html__( 'Cart is not available.', 'wpfnl' ) . '</li></ul>' ) );
1832 }
1833
1834 WC()->cart->remove_coupon( $coupon_code );
1835 WC()->cart->calculate_totals();
1836
1837 ob_start();
1838 wc_print_notices();
1839 $messages = ob_get_clean();
1840
1841 wp_send_json_success( array( 'messages' => $messages ) );
1842 }
1843
1844
1845 /**
1846 * Render shipping methods HTML for modern checkout custom section.
1847 *
1848 * @return void
1849 */
1850 public function wpfnl_cart_totals_shipping_html() {
1851 if ( ! Wpfnl_functions::is_wc_active() || ! function_exists( 'WC' ) ) {
1852 return;
1853 }
1854
1855 $packages = WC()->shipping()->get_packages();
1856 if ( empty( $packages ) ) {
1857 WC()->cart->calculate_totals();
1858 $packages = WC()->shipping()->get_packages();
1859 }
1860
1861 $first = true;
1862 foreach ( $packages as $i => $package ) {
1863 $chosen_method = isset( WC()->session->chosen_shipping_methods[ $i ] ) ? WC()->session->chosen_shipping_methods[ $i ] : '';
1864 $product_names = array();
1865
1866 if ( count( $packages ) > 1 ) {
1867 foreach ( $package['contents'] as $item_id => $values ) {
1868 $product_names[ $item_id ] = $values['data']->get_name() . ' &times;' . $values['quantity'];
1869 }
1870 $product_names = apply_filters( 'woocommerce_shipping_package_details_array', $product_names, $package );
1871 }
1872
1873 include WPFNL_DIR . 'public/modules/checkout/templates/checkout/shipping-methods.php';
1874 do_action( 'woocommerce_review_order_after_shipping' );
1875 $first = false;
1876 }
1877 }
1878
1879
1880 /**
1881 * Check if current checkout uses modern templates.
1882 *
1883 * @return bool
1884 */
1885 private function is_modern_checkout_layout() {
1886 $checkout_id = 0;
1887
1888 if ( wp_doing_ajax() ) {
1889 if ( isset( $_POST['step_id'] ) ) {
1890 $checkout_id = intval( wp_unslash( $_POST['step_id'] ) );
1891 } elseif ( isset( $_POST['_wpfunnels_checkout_id'] ) ) {
1892 $checkout_id = intval( wp_unslash( $_POST['_wpfunnels_checkout_id'] ) );
1893 } elseif ( isset( $_POST['post_data'] ) ) {
1894 $post_data = array();
1895 parse_str( wp_unslash( $_POST['post_data'] ), $post_data );
1896 if ( isset( $post_data['_wpfunnels_checkout_id'] ) ) {
1897 $checkout_id = intval( $post_data['_wpfunnels_checkout_id'] );
1898 }
1899 }
1900 }
1901
1902 if ( ! $checkout_id ) {
1903 $checkout_id = get_the_ID();
1904 }
1905
1906 if ( ! $checkout_id ) {
1907 return false;
1908 }
1909
1910 $checkout_layout = Wpfnl::get_instance()->meta->get_checkout_meta_value( $checkout_id, 'wpfnl_checkout_layout', 'wpfnl-col-2' );
1911
1912 return in_array( $checkout_layout, array( 'wpfnl-modern-checkout', 'wpfnl-modern-one-column' ), true );
1913 }
1914
1915
1916 /**
1917 * Set checkout ID hidden field in checkout page.
1918 *
1919 * @param array $checkout
1920 *
1921 * @return void
1922 */
1923 public function wpfnl_set_checkout_id( $checkout ) {
1924 global $post;
1925 $checkout_id = 0;
1926 if ( Wpfnl_functions::is_funnel_checkout_page() ) {
1927 if( isset($post->ID) ){
1928 $checkout_id = $post->ID;
1929 }else{
1930 $checkout_id = isset($_POST['step_id']) ? $_POST['step_id'] : '';
1931 }
1932
1933 }
1934 if( $checkout_id ){
1935 echo '<input type="hidden" class="input-hidden _wpfunnels_checkout_id" name="_wpfunnels_checkout_id" value="' . intval( $checkout_id ) . '">';
1936 }
1937 }
1938
1939
1940 /**
1941 * Redirect funnel checkout after login in checkout page
1942 *
1943 * @param $redirect_url
1944 * @param $user
1945 *
1946 * @return false|string
1947 */
1948 public function wpfnl_redirect_after_login( $redirect_url, $user ) {
1949
1950 if (isset($_POST['_wpfunnels_checkout_id'])) {
1951 $funnel_checkout_id = $_POST['_wpfunnels_checkout_id'];
1952 $redirect_url = get_the_permalink($funnel_checkout_id);
1953 }
1954 return $redirect_url;
1955
1956 }
1957
1958
1959 /**
1960 * Get Custom Woocommerce template
1961 *
1962 * @param $template
1963 * @param $template_name
1964 * @param $template_path
1965 *
1966 * @return mixed|string
1967 */
1968
1969 public static function wpfunnels_woocommerce_locate_template($template, $template_name, $template_path)
1970 {
1971 /***
1972 * Fires when change the wc template
1973 *
1974 * @since 2.8.21
1975 */
1976 if( apply_filters( 'wpfunnels/maybe_locate_template', true ) ){
1977 global $woocommerce;
1978 $_template = $template;
1979 $plugin_path = WPFNL_DIR . '/woocommerce/templates/';
1980
1981 if (file_exists($plugin_path . $template_name)) {
1982 $template = $plugin_path . $template_name;
1983 }
1984
1985 if ( ! $template ) {
1986 $template = $_template;
1987 }
1988 }
1989 return $template;
1990 }
1991
1992
1993 /**
1994 * Discount on checkout
1995 *
1996 * @param $cart_object
1997 */
1998 public function wpfnl_checkout_discount( $cart_object ){
1999
2000 if( !$this->is_funnel_checkout_product_available_on_cart() ){
2001 return;
2002 }
2003
2004 if ( is_admin() && wp_doing_ajax() ){
2005 return;
2006 }
2007 if( isset( $_POST['wpfnl_offer_page'] ) ){
2008 return;
2009 }
2010
2011 $step_id = Wpfnl_functions::get_checkout_id_from_post( $_POST );
2012
2013 if( !$step_id ){
2014 $step_id = isset($_POST['step_id']) ? $_POST['step_id'] : '';
2015 }
2016
2017 if( !$step_id ){
2018 $step_id = isset($_POST['_wpfunnels_checkout_id']) ? $_POST['_wpfunnels_checkout_id'] : '';
2019 }
2020
2021 if( !$step_id ) {
2022 return;
2023 }
2024
2025 $funnel_id = Wpfnl_functions::get_funnel_id_from_step( $step_id );
2026
2027 if( !$funnel_id ){
2028 return;
2029 }
2030
2031 $discount_instance = new WpfnlDiscount();
2032
2033 if( $discount_instance->maybe_time_bound_discount( $step_id ) && !$discount_instance->maybe_validate_discount_time( $step_id )) {
2034 return;
2035 }
2036
2037 $discount = $discount_instance->get_discount_settings( $step_id );
2038
2039 if( !is_array($discount) ){
2040 return;
2041 }
2042
2043 if( (isset($discount['discountOptions']) && $discount['discountOptions'] !== 'original') ){
2044 if( 'regular' === $discount['discountapplyto'] ){
2045 foreach (WC()->cart->get_cart() as $cart_item_key => $cart_item) {
2046 $product = $cart_item['data'];
2047 if (isset($cart_item['bundled_by']) || $product->is_type('bundle') ) { // 'bundled_by' is a common meta key used for bundled items
2048 $regular_price = $product->get_regular_price();
2049 $regular_price = $regular_price ? $regular_price : $product->get_price();
2050 $cart_item['data']->set_price($regular_price);
2051 }
2052 }
2053 }
2054
2055 $helper = CheckoutHelper::getInstance();
2056 $total = $helper->get_custom_subtotal();
2057 $discount_amount = $discount_instance->get_discount_amount( $discount['discountOptions'], $discount['mutedDiscountValue'], $total );
2058 $discount_amount = filter_var($discount_amount, FILTER_SANITIZE_NUMBER_FLOAT, FILTER_FLAG_ALLOW_FRACTION);
2059 $discount_amount = apply_filters('wpfunnels/checkout_discount_amount', $discount_amount);
2060 $discount_amount = floatval($discount_amount);
2061 $cart_object->add_fee( __('Discount','wpfnl'), -$discount_amount, true,'standard');
2062 }
2063 }
2064
2065
2066 /**
2067 * Check whether funnel checkout product is available on cart or not
2068 *
2069 * @return boolean true if funnel checkout product is available on cart, false otherwise
2070 *
2071 * @access public
2072 * @since 3.4.15
2073 */
2074 public function is_funnel_checkout_product_available_on_cart() {
2075 if( WC()->cart->is_empty() ){
2076 return false;
2077 }
2078 $is_available = false;
2079 foreach (WC()->cart->get_cart() as $cart_item_key => $cart_item) {
2080 if( !isset( $cart_item['wpfnl_order_bump']) ){
2081 $is_available = true;
2082 break;
2083 }
2084 }
2085 return $is_available;
2086 }
2087
2088
2089 /**
2090 * Get price type
2091 */
2092 public function get_product_price_type( $funnel_id, $step_id ){
2093
2094 $is_gbf = get_post_meta( $funnel_id, 'is_global_funnel', true );
2095 if( $is_gbf == 'no' || !$is_gbf ){
2096 $discount_instance = new WpfnlDiscount();
2097 if( $discount_instance->maybe_time_bound_discount( $step_id ) && !$discount_instance->maybe_validate_discount_time( $step_id ) ) {
2098 return 'original';
2099 }
2100
2101 $discount = $discount_instance->get_discount_settings( $step_id );
2102 if( is_array($discount) ){
2103 if( $discount['discountOptions'] !== 'original' ){
2104 if( $discount['discountapplyto'] == 'regular' ){
2105 return 'regular';
2106 }else{
2107 return 'sale';
2108 }
2109 }
2110 }
2111 }
2112 return 'original';
2113 }
2114
2115 /**
2116 * After optin form submition set cookie data
2117 * Use cookes data for checkout billings fields
2118 *
2119 * @param $step_id
2120 * @param $post_action
2121 * @param $action_type
2122 * @param $record
2123 */
2124
2125 public function get_optin_data_checkout($step_id, $post_action, $action_type, $record){
2126 if(isset($record->form_data['data_to_checkout']) && 'yes' == $record->form_data['data_to_checkout']){
2127 /**
2128 * Set Cookie Data
2129 */
2130 $cookie_name = 'wpfunnels_send_data_checkout';
2131 $cookie = isset( $_COOKIE[$cookie_name] ) ? json_decode( wp_unslash( $_COOKIE[$cookie_name] ), true ) : array();
2132 $cookie['after_optin_submit_send_for_checkout'] = ( isset($record->form_data['email']) && $record->form_data['email'] ) ? $record->form_data : false;
2133 ob_start();
2134 setcookie( $cookie_name, wp_json_encode( $cookie ), time() + 3600 * 6, '/', COOKIE_DOMAIN );
2135 ob_end_flush();
2136 }
2137 }
2138
2139 /**
2140 * Get Optin cookes data in checkout fields
2141 *
2142 * @param $fields
2143 *
2144 * @return array
2145 */
2146 public function set_option_data_in_checkout_filed($fields)
2147 {
2148 $cookie_name = 'wpfunnels_send_data_checkout';
2149 $cookie = isset( $_COOKIE[$cookie_name] ) ? json_decode( wp_unslash( $_COOKIE[$cookie_name] ), true ) : array();
2150 if(!empty($cookie)){
2151 if(isset($cookie['after_optin_submit_send_for_checkout'])){
2152 $first_name = isset($cookie['after_optin_submit_send_for_checkout']['first_name']) ? $cookie['after_optin_submit_send_for_checkout']['first_name'] : '';
2153 $last_name = isset($cookie['after_optin_submit_send_for_checkout']['last_name']) ? $cookie['after_optin_submit_send_for_checkout']['last_name'] : '';
2154 $phone = isset($cookie['after_optin_submit_send_for_checkout']['phone']) ? $cookie['after_optin_submit_send_for_checkout']['phone'] : '';
2155 $email = isset($cookie['after_optin_submit_send_for_checkout']['email']) ? $cookie['after_optin_submit_send_for_checkout']['email'] : '';
2156 $fields['billing']['billing_first_name']['default'] = $first_name;
2157 $fields['billing']['billing_last_name']['default'] = $last_name;
2158 $fields['billing']['billing_phone']['default'] = $phone;
2159 $fields['billing']['billing_email']['default'] = $email;
2160 }
2161 }
2162
2163 $checkout_id = 0;
2164
2165 if ( wp_doing_ajax() ) {
2166 if ( isset( $_POST['step_id'] ) ) {
2167 $checkout_id = intval( wp_unslash( $_POST['step_id'] ) );
2168 } elseif ( isset( $_POST['_wpfunnels_checkout_id'] ) ) {
2169 $checkout_id = intval( wp_unslash( $_POST['_wpfunnels_checkout_id'] ) );
2170 } elseif ( isset( $_POST['post_data'] ) ) {
2171 $post_data = array();
2172 parse_str( wp_unslash( $_POST['post_data'] ), $post_data );
2173 if ( isset( $post_data['_wpfunnels_checkout_id'] ) ) {
2174 $checkout_id = intval( $post_data['_wpfunnels_checkout_id'] );
2175 }
2176 }
2177 }
2178
2179 if ( ! $checkout_id ) {
2180 $checkout_id = get_the_ID();
2181 }
2182
2183 if ( $checkout_id ) {
2184 $checkout_layout = Wpfnl::get_instance()->meta->get_checkout_meta_value( $checkout_id, 'wpfnl_checkout_layout', 'wpfnl-col-2' );
2185
2186 if ( 'wpfnl-modern-checkout' === $checkout_layout ) {
2187 if ( isset( $fields['account']['account_username'] ) ) {
2188 unset( $fields['account']['account_username'] );
2189 }
2190 if ( isset( $fields['account']['account_password'] ) ) {
2191 unset( $fields['account']['account_password'] );
2192 }
2193 }
2194 }
2195
2196 return $fields;
2197 }
2198
2199
2200
2201 /**
2202 * Modifies the available payment gateways based on certain conditions.
2203 *
2204 * @param array $available_gateways The array of available payment gateways.
2205 * @return array The modified array of available payment gateways.
2206 *
2207 * @since 3.1.8
2208 */
2209 public function conditional_payment_gateways( $available_gateways ){
2210 if( defined('WPFNL_PRO_VERSION') ){
2211 $is_checkout = Wpfnl_functions::is_funnel_checkout_page();
2212 if( $is_checkout['status'] ){
2213 $is_funnel_checkout = Wpfnl_functions::check_if_this_is_step_type_by_id( $is_checkout['id'], 'checkout' );
2214 if( $is_funnel_checkout ){
2215 $disableGateways = get_post_meta($is_checkout['id'], '_wpfnl_disabled_payemnts', true);
2216 if( is_array($disableGateways) && !empty($disableGateways ) ){
2217 foreach( $available_gateways as $key=>$gateway ){
2218 if (in_array($key, $disableGateways)){
2219 unset($available_gateways[$key]);
2220 }
2221 }
2222 }
2223
2224 }
2225 }
2226 }
2227 return $available_gateways;
2228 }
2229 }
2230