PluginProbe
WPFunnels – Funnel Builder for WooCommerce with Checkout & One Click Upsell / 3.11.1
WPFunnels – Funnel Builder for WooCommerce with Checkout & One Click Upsell v3.11.1
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.11.1, at public/modules/checkout/class-wpfnl-checkout.php

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