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

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