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

2,892 lines 98.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Checkout module
4 *
5 * @package
6 * @since 2.7.9
7 */
8
9 namespace WPFunnels\Modules\Frontend\Checkout;
10
11 use Error;
12 use WPFunnels\Data_Store\Wpfnl_Steps_Store_Data;
13 use WPFunnels\Frontend\Module\Wpfnl_Frontend_Module;
14 use WPFunnels\Frontend\Wpfnl_Public;
15 use WPFunnels\Meta\Wpfnl_Default_Meta;
16 use WPFunnels\Wpfnl;
17 use WPFunnels\Wpfnl_functions;
18 use WPFunnels\lms\helper\Wpfnl_lms_learndash_functions;
19 use WPFunnels\Modules\Frontend\Checkout\Variable\Wpfnl_Variable_Product;
20 use WPFunnels\Modules\Frontend\Checkout\Single\Wpfnl_Single_Product;
21 use function cli\err;
22 use Wpfnl_Public_Type_Factory;
23 use WPFunnels\Conditions\Wpfnl_Condition_Checker;
24 use WPFunnels\Discount\WpfnlDiscount;
25 use WPFunnels\Modules\Frontend\CheckoutHelper\CheckoutHelper;
26
27 // Include Product Selection class
28 require_once WPFNL_DIR . 'public/modules/checkout/class-wpfnl-product-selection.php';
29
30 class Module extends Wpfnl_Frontend_Module
31 {
32 public $funnel_id;
33
34 public $step_id;
35
36 /**
37 * Whether the funnel hidden fields have already been printed for this request.
38 *
39 * @var bool
40 */
41 private $checkout_metas_rendered = false;
42
43 /**
44 * Whether the additional information (order notes) block has already rendered.
45 *
46 * @var bool
47 */
48 private $additional_fields_rendered = false;
49
50 // const CHECKOUT = "";
51
52 public function __construct()
53 {
54
55 /* set checkout flag */
56 add_filter('woocommerce_is_checkout', [$this, 'checkout_flag'], 9999);
57
58 /* suppress WC "session expired" AJAX response when rendering checkout with empty cart for preview */
59 add_filter('woocommerce_checkout_update_order_review_expired', [$this, 'allow_empty_cart_order_review']);
60
61 if( !defined('WCML_VERSION') ){
62 add_action('woocommerce_checkout_update_order_meta', [$this, 'save_checkout_fields'], 10, 2);
63 }
64
65 /* initialize cart data */
66 add_action( 'wp', [ $this, 'initialize_cart_data' ] );
67
68 /* init WooCommerce action */
69 add_action( 'wp', [$this, 'init_actions']);
70
71 /* Exclude Funnel Builder hooks when WPFunnels checkout is active */
72 add_action( 'wp', [$this, 'exclude_funnel_builder_hooks'], 1 );
73
74 /* Remove CartFlows global checkout override before it fires on 'wp' priority 0 */
75 add_action( 'template_redirect', [$this, 'exclude_funnel_builder_hooks'], 0 );
76
77 /* Exclude conflicting hooks during checkout AJAX (update_order_review) */
78 add_action( 'woocommerce_checkout_update_order_review', [$this, 'exclude_funnel_builder_hooks_on_ajax'], -1 );
79
80 /*
81 * Prevent CartFlows from overriding the global checkout on WPFunnels step pages.
82 * CartFlows calls should_show_global_checkout() inside override_global_checkout()
83 * and global_checkout_template_redirect() — returning true from this filter
84 * makes both methods bail out immediately, regardless of object reference issues.
85 */
86 add_filter( 'cartflows_allow_display_global_checkout', [$this, 'prevent_cartflows_global_checkout_override'], 10, 2 );
87
88 /*
89 * Prevent FunnelKit from redirecting to its own global checkout on WPFunnels
90 * store checkout pages. fk_checkout_redirect_resolve fires on this filter and
91 * WFACP_Common caches the result — intercept early at priority 5 (before FK's
92 * priority 8 and 10) and return 0 to signal "no override" when on a WPFunnels page.
93 */
94 add_filter( 'wfacp_global_checkout_page_id', [$this, 'prevent_funnelkit_global_checkout_override'], 5, 1 );
95
96 /* register checkout shortcode */
97 add_shortcode('wpfunnels_checkout', array($this, 'render_checkout_shortcode'));
98 add_action('wp_ajax_wpfnl_next_button_ajax', [$this, 'wpfnl_next_button_ajax']);
99 add_action('wp_ajax_nopriv_wpfnl_next_button_ajax', [$this, 'wpfnl_next_button_ajax']);
100
101 add_action( 'wp_ajax_wpfnl_order_bump_ajax', [$this, 'wpfnl_order_bump_ajax']);
102 add_action( 'wp_ajax_nopriv_wpfnl_order_bump_ajax', [$this, 'wpfnl_order_bump_ajax']);
103 add_action( 'wp_ajax_wpfnl_get_course_details', [$this, 'wpfnl_get_course_details']);
104 add_action( 'wp_ajax_nopriv_wpfnl_get_course_details', [$this, 'wpfnl_get_course_details']);
105
106 add_action( 'wp_ajax_wpfnl_update_variation', [$this, 'wpfnl_update_variation']);
107 add_action( 'wp_ajax_nopriv_wpfnl_update_variation', [$this, 'wpfnl_update_variation']);
108 add_action( 'wp_ajax_wpfnl_apply_checkout_coupon', [$this, 'wpfnl_apply_checkout_coupon'] );
109 add_action( 'wp_ajax_nopriv_wpfnl_apply_checkout_coupon', [$this, 'wpfnl_apply_checkout_coupon'] );
110 add_action( 'wp_ajax_wpfnl_remove_checkout_coupon', [$this, 'wpfnl_remove_checkout_coupon'] );
111 add_action( 'wp_ajax_nopriv_wpfnl_remove_checkout_coupon', [$this, 'wpfnl_remove_checkout_coupon'] );
112
113 // Email existence checker for modern checkout customer-info flow.
114 add_action( 'wp_ajax_wpf_check_email_exists', [$this, 'wpfnl_check_email_exists'] );
115 add_action( 'wp_ajax_nopriv_wpf_check_email_exists', [$this, 'wpfnl_check_email_exists'] );
116 add_action( 'wp_ajax_wpfnl_check_email_exists', [$this, 'wpfnl_check_email_exists'] );
117 add_action( 'wp_ajax_nopriv_wpfnl_check_email_exists', [$this, 'wpfnl_check_email_exists'] );
118
119 add_action( 'woocommerce_before_calculate_totals', [$this, 'custom_price_to_cart_item'], 9999);
120 add_action( 'woocommerce_before_checkout_form', [$this, 'apply_auto_coupon'], 9999);
121
122 add_action( 'wp_ajax_nopriv_wpfnl_checkout_cart', [$this, 'wpfnl_checkout_cart']);
123 add_filter('woocommerce_cart_product_out_of_stock_message', [$this,'wpfnl_out_of_stock_message_in_checkout'], 10, 2);
124
125 add_action( 'wpfunnels/before_checkout_form', array($this, 'before_checkout_form_actions') );
126
127 add_action( 'woocommerce_after_order_notes', array( $this, 'wpfnl_set_checkout_id' ), 999 );
128 add_action( 'woocommerce_login_form_end', array( $this, 'wpfnl_set_checkout_id' ), 999 );
129 add_filter( 'woocommerce_login_redirect', [$this,'wpfnl_redirect_after_login'], 10, 2 );
130 add_action( 'woocommerce_before_calculate_totals', [$this,'wpfnl_checkout_discount'], 10);
131 add_action( 'woocommerce_cart_calculate_fees', [$this,'wpfnl_checkout_discount'], 10);
132
133 add_action( 'wpfunnels/after_optin_submit', array( $this, 'get_optin_data_checkout' ), 10, 4 );
134 add_filter( 'woocommerce_checkout_fields', array($this, 'set_option_data_in_checkout_filed'),10 );
135 add_filter( 'woocommerce_available_payment_gateways', array($this, 'conditional_payment_gateways'),10 );
136 add_action( 'woocommerce_checkout_after_customer_details', array( $this, 'add_custom_shipping_section' ), 10 );
137 add_action( 'wpfunnels/after_order_total', array( $this, 'display_custom_coupon_field' ), 5 );
138 add_filter( 'woocommerce_update_order_review_fragments', array( $this, 'split_after_order_total_fragment' ) );
139 add_filter( 'woocommerce_update_order_review_fragments', array( $this, 'wpfnl_update_checkout_fragments' ), 20 );
140
141 /* Manage Express Checkout buttons visibility dynamically */
142 add_filter( 'wc_stripe_show_payment_request_on_checkout', [$this, 'wpfnl_manage_stripe_express_checkout'], 9999, 2 );
143
144 /* Position Express Checkout buttons based on step setting */
145 add_action( 'wp', [$this, 'wpfnl_position_stripe_express_checkout'], 5 );
146
147 }
148
149 /**
150 * Manage Stripe Express Checkout buttons visibility based on WPFunnels step settings.
151 *
152 * @param bool $show If it should show
153 * @param \WP_Post $post Current post
154 * @return bool
155 */
156 public function wpfnl_manage_stripe_express_checkout( $show, $post = null ) {
157 if ( ! Wpfnl_functions::check_if_this_is_step_type('checkout') ) {
158 return $show;
159 }
160
161 $checkout_id = '';
162 if( wp_doing_ajax() ){
163 $checkout_id = Wpfnl_functions::get_checkout_id_from_post($_POST);
164 } else {
165 global $post;
166 if ( $post && isset( $post->ID ) ) {
167 $checkout_id = $post->ID;
168 }
169 }
170
171 if ( $checkout_id ) {
172 $express_checkout_enabled = get_post_meta( $checkout_id, '_wpfnl_express_checkout_enabled', true );
173 if ( 'yes' !== $express_checkout_enabled ) {
174 return false; // Hide Stripe Payment Request (Apple Pay / Google Pay)
175 }
176 }
177
178 return $show;
179 }
180
181 /**
182 * Reposition Stripe Express Checkout buttons based on the `_wpfnl_express_checkout_position`
183 * step meta. Detaches Stripe's default hook and re-attaches it to the user-selected position.
184 *
185 * Supported positions:
186 * - top -> woocommerce_checkout_before_customer_details (default Stripe pos)
187 * - before_product_switcher -> woocommerce_before_order_notes, priority 4
188 * - after_product_switcher -> woocommerce_before_order_notes, priority 6
189 * - before_order_summary -> woocommerce_checkout_before_order_review
190 * - after_order_summary -> woocommerce_checkout_after_order_review
191 * - above_payment_gateways -> woocommerce_review_order_before_payment
192 *
193 * @return void
194 */
195 public function wpfnl_position_stripe_express_checkout() {
196 if ( ! Wpfnl_functions::check_if_this_is_step_type('checkout') ) {
197 return;
198 }
199
200 global $post;
201 $checkout_id = $post && isset( $post->ID ) ? $post->ID : 0;
202 if ( ! $checkout_id ) {
203 return;
204 }
205
206 $express_checkout_enabled = get_post_meta( $checkout_id, '_wpfnl_express_checkout_enabled', true );
207 // Only reposition when the feature is enabled. 'yes' is the default.
208 if ( 'yes' !== $express_checkout_enabled ) {
209 return;
210 }
211
212 $position = get_post_meta( $checkout_id, '_wpfnl_express_checkout_position', true );
213 if ( ! $position ) {
214 $position = 'top'; // default
215 }
216
217 // No repositioning needed for the default 'top' position — Stripe already hooks there.
218 if ( 'top' === $position ) {
219 return;
220 }
221
222 // Retrieve the Stripe ECE singleton (hooks are already registered since we are on 'wp' action).
223 if ( ! class_exists( '\WC_Stripe_Express_Checkout_Element' ) ) {
224 return;
225 }
226
227 $ece = \WC_Stripe_Express_Checkout_Element::instance();
228 if ( ! $ece ) {
229 return;
230 }
231
232 // Detach from Stripe's default checkout position.
233 remove_action( 'woocommerce_checkout_before_customer_details', [ $ece, 'display_express_checkout_button_html' ], 1 );
234
235 // Map setting → hook name + priority.
236 $hook_map = array(
237 'before_product_switcher' => array( 'woocommerce_before_order_notes', 4 ),
238 'after_product_switcher' => array( 'woocommerce_before_order_notes', 6 ),
239 'before_order_summary' => array( 'woocommerce_checkout_before_order_review', 10 ),
240 'after_order_summary' => array( 'woocommerce_checkout_after_order_review', 10 ),
241 'above_payment_gateways' => array( 'woocommerce_review_order_before_payment', 10 ),
242 );
243
244 if ( isset( $hook_map[ $position ] ) ) {
245 list( $hook, $priority ) = $hook_map[ $position ];
246 add_action( $hook, [ $ece, 'display_express_checkout_button_html' ], $priority );
247 }
248 }
249
250
251
252
253 /**
254 * Set checkout flag to true if this is checkout step type
255 *
256 * @param $is_checkout
257 *
258 * @return bool
259 *
260 * @since 1.0.0
261 */
262 public function checkout_flag($is_checkout)
263 {
264 if (Wpfnl_functions::check_if_this_is_step_type('checkout') && ! Wpfnl_functions::check_if_this_is_lms_step() ) {
265 $is_checkout = true;
266 }
267 return $is_checkout;
268 }
269
270
271 /**
272 * Prevent WooCommerce from returning "session expired" on the update_order_review
273 * AJAX when the cart is empty because we're rendering a Store Checkout preview.
274 *
275 * @param bool $expired
276 * @return bool
277 */
278 public function allow_empty_cart_order_review( $expired ) {
279 if ( WC()->session && WC()->session->get( 'wpfnl_checkout_preview_mode' ) ) {
280 return false;
281 }
282 return $expired;
283 }
284
285 /**
286 * Prevent CartFlows from overriding the global checkout page when the current
287 * page is a WPFunnels step. CartFlows checks this filter inside both
288 * override_global_checkout() and global_checkout_template_redirect() — returning
289 * true makes both bail out immediately without touching $GLOBALS['post'].
290 *
291 * @param bool $is_allow
292 * @param int $checkout_id Current post ID passed by CartFlows.
293 * @return bool
294 * @since 3.10.6
295 */
296 public function prevent_cartflows_global_checkout_override( $is_allow, $checkout_id ) {
297 if ( $is_allow ) {
298 return $is_allow;
299 }
300
301 // If the current post is a WPFunnels step, tell CartFlows to skip its override.
302 if ( $checkout_id && Wpfnl_functions::is_funnel_step_page() ) {
303 return true;
304 }
305
306 return $is_allow;
307 }
308
309 /**
310 * Prevent FunnelKit from redirecting to its own global checkout on WPFunnels
311 * store checkout pages.
312 *
313 * fk_checkout_redirect_resolve (priority 10) and maybe_override_global_checkout_id
314 * (priority 8) both fire on wfacp_global_checkout_page_id after this callback.
315 * WordPress passes each callback's return value to the next, so returning 0 here
316 * is not enough — priority 8/10 will overwrite it with a real step ID.
317 *
318 * The fix: when on a WPFunnels step page, remove those downstream filters from
319 * inside this callback (they haven't run yet at priority 5), then return 0.
320 * WFACP_Common treats 0 as "no global checkout configured" → skips the redirect.
321 *
322 * @param int $checkout_page_id
323 * @return int
324 * @since 3.10.6
325 */
326 public function prevent_funnelkit_global_checkout_override( $checkout_page_id ) {
327 if ( ! Wpfnl_functions::is_funnel_step_page() ) {
328 return $checkout_page_id;
329 }
330
331 // Remove FunnelKit's downstream filters before they run (we are at priority 5).
332 if ( class_exists( 'WFFN_Step_WC_Checkout' ) ) {
333 $wffn_checkout = \WFFN_Step_WC_Checkout::get_instance();
334 if ( $wffn_checkout ) {
335 remove_filter( 'wfacp_global_checkout_page_id', array( $wffn_checkout, 'maybe_override_global_checkout_id' ), 8 );
336 remove_filter( 'wfacp_global_checkout_page_id', array( $wffn_checkout, 'fk_checkout_redirect_resolve' ), 10 );
337 }
338 }
339
340 // Return 0 — WFACP_Common treats 0 as "no global checkout configured".
341 return 0;
342 }
343
344
345 /**
346 * Remove conflicting hooks from Funnel Builder (WFACP/FunnelKit) and CartFlows
347 * that interfere with WPFunnels checkout pages.
348 *
349 * Two entry points:
350 * 1. 'template_redirect' (priority -1) — fires before 'wp', catches CartFlows
351 * global checkout override which runs at wp priority 0.
352 * 2. 'wp' (priority 1) — catches remaining hooks that need $post to be set.
353 *
354 * @since 3.10.6
355 */
356 public function exclude_funnel_builder_hooks() {
357 // Bail early if not a WPFunnels step page at all
358 if ( ! Wpfnl_functions::is_funnel_step_page() ) {
359 return;
360 }
361
362 $this->remove_conflicting_plugin_hooks();
363 }
364
365 /**
366 * Exclude conflicting hooks during the woocommerce_checkout_update_order_review
367 * AJAX call, where is_funnel_step_page() cannot rely on get_post_type().
368 * Uses _wpfunnels_checkout_id in POST data to detect WPFunnels context.
369 *
370 * @since 3.10.6
371 */
372 public function exclude_funnel_builder_hooks_on_ajax() {
373 $checkout_id = Wpfnl_functions::get_checkout_id_from_post( $_POST );
374 if ( ! $checkout_id ) {
375 return;
376 }
377 if ( ! Wpfnl_functions::check_if_this_is_step_type_by_id( $checkout_id, 'checkout' ) ) {
378 return;
379 }
380
381 $this->remove_conflicting_plugin_hooks();
382 }
383
384 /**
385 * Core removal logic shared by both page-load and AJAX entry points.
386 *
387 * @since 3.10.6
388 */
389 private function remove_conflicting_plugin_hooks() {
390 // --- FunnelKit (WFACP) ---
391 if ( class_exists( 'WFFN_Step_WC_Checkout' ) ) {
392 $wffn_checkout = \WFFN_Step_WC_Checkout::get_instance();
393 if ( $wffn_checkout ) {
394 // Prevents FunnelKit store checkout from overriding WPFunnels checkout page ID
395 remove_filter( 'wfacp_global_checkout_page_id', array( $wffn_checkout, 'maybe_override_global_checkout_id' ), 8 );
396 // Prevents FK cart-based checkout redirect from firing on WPFunnels pages
397 remove_filter( 'wfacp_global_checkout_page_id', array( $wffn_checkout, 'fk_checkout_redirect_resolve' ), 10 );
398 // Prevents FunnelKit from setting up its own funnel session on WPFunnels AJAX
399 remove_action( 'woocommerce_checkout_update_order_review', array( $wffn_checkout, 'setup_funnel_on_update_order' ), 99 );
400 }
401 }
402
403 // --- FunnelKit (WFSWT) ---
404 if ( class_exists( 'WFFN_Step_WC_Thankyou' ) ) {
405 $wffn_thankyou = \WFFN_Step_WC_Thankyou::get_instance();
406 if ( $wffn_thankyou ) {
407 // Prevents FunnelKit store checkout from overriding WPFunnels checkout page ID
408 remove_filter( 'wffn_wfty_filter_page_ids', array( $wffn_thankyou, 'maybe_filter_thankyou' ), 10 );
409 }
410 }
411
412 // --- CartFlows: global checkout page override ---
413 if ( class_exists( 'Cartflows_Global_Checkout' ) ) {
414 $cartflows_global = \Cartflows_Global_Checkout::get_instance();
415 if ( $cartflows_global ) {
416 // Prevents CartFlows from replacing $GLOBALS['post'] with its own checkout
417 remove_action( 'wp', array( $cartflows_global, 'override_global_checkout' ), 0 );
418 remove_action( 'wp', array( $cartflows_global, 'maybe_override_order_pay_page' ), 0 );
419 // Prevents CartFlows from redirecting to its global checkout URL
420 remove_action( 'template_redirect', array( $cartflows_global, 'global_checkout_template_redirect' ), 1 );
421 }
422 }
423
424 // --- CartFlows: checkout markup hooks ---
425 if ( class_exists( 'Cartflows_Checkout_Markup' ) ) {
426 $cartflows_markup = \Cartflows_Checkout_Markup::get_instance();
427 if ( $cartflows_markup ) {
428 // Prevents CartFlows from hijacking the woocommerce_is_checkout flag
429 remove_filter( 'woocommerce_is_checkout', array( $cartflows_markup, 'woo_checkout_flag' ), 9999 );
430 // Prevents CartFlows from emptying and repopulating the cart with its own products
431 remove_action( 'wp', array( $cartflows_markup, 'preconfigured_cart_data' ), 1 );
432 // Prevents CartFlows from loading its scripts/hooks on WPFunnels pages
433 remove_action( 'wp', array( $cartflows_markup, 'shortcode_load_data' ), 999 );
434 // Prevents CartFlows from restoring a different checkout's cart during AJAX
435 remove_action( 'woocommerce_checkout_update_order_review', array( $cartflows_markup, 'restore_cart_data' ) );
436 // Prevents CartFlows from overwriting WPFunnels cart item prices
437 remove_action( 'woocommerce_before_calculate_totals', array( $cartflows_markup, 'custom_price_to_cart_item' ), 9999 );
438 }
439 }
440 }
441
442
443 /**
444 * Init necessary actions related Checkout step
445 */
446 public function init_actions() {
447 add_action( 'woocommerce_after_order_notes', [$this, 'checkout_shortcode_metas']);
448 add_action( 'woocommerce_before_order_notes', [$this, 'mark_additional_fields_rendered'], 1 );
449 // add_action( 'woocommerce_checkout_after_customer_details', [$this, 'maybe_render_additional_fields'], 15 );
450 add_action( 'woocommerce_checkout_after_customer_details', [$this, 'maybe_render_checkout_metas'], 20 );
451 $this->display_coupon_field();
452 $this->enable_checkout_hook();
453
454 if ( Wpfnl_functions::check_if_this_is_step_type( 'checkout' ) ) {
455 add_action( 'wp_enqueue_scripts', array( $this, 'load_checkout_step_scripts' ) );
456 }
457 }
458
459
460 /**
461 * Load script/styles related to checkout step
462 *
463 * @since 3.1.3
464 */
465 public function load_checkout_step_scripts() {
466 $this->load_google_map_api_scripts();
467 $this->load_tokenization_form_script();
468 }
469
470
471 /**
472 * Load WooCommerce's saved payment method (tokenization) script on checkout steps.
473 *
474 * WooCommerce enqueues `woocommerce-tokenization-form` from inside
475 * WC_Payment_Gateway::payment_fields(). On a funnel checkout the payment box is often
476 * rendered for the first time by wc-ajax=update_order_review, where wp_enqueue_script()
477 * is a no-op, so the saved payment method markup arrives without the script that selects
478 * "Use a new payment method". Stripe's UPE script then reads the unchecked
479 * #wc-stripe-payment-token-new radio as "customer picked a saved card", never creates a
480 * PaymentMethod, and the order is placed with an empty wc-stripe-payment-method field.
481 *
482 * Enqueueing it up front on every checkout step keeps the AJAX rendered markup working.
483 *
484 * @since 3.13.2
485 */
486 private function load_tokenization_form_script() {
487 if ( ! Wpfnl_functions::is_wc_active() || ! function_exists( 'WC' ) ) {
488 return;
489 }
490
491 $gateways = WC()->payment_gateways() ? WC()->payment_gateways()->payment_gateways() : [];
492
493 foreach ( $gateways as $gateway ) {
494 if ( 'yes' === $gateway->enabled && $gateway->supports( 'tokenization' ) ) {
495 // Also localises wc_tokenization_form_params, which the script reads.
496 $gateway->tokenization_script();
497 break;
498 }
499 }
500 }
501
502
503 /**
504 * Load google map places script on checkout page
505 *
506 * @since 3.1.3
507 */
508 public function load_google_map_api_scripts() {
509 $google_map_api_key = Wpfnl_functions::get_google_map_api_key();
510 if ( !$google_map_api_key ) {
511 return;
512 }
513
514 if( !Wpfnl_functions::validate_google_places_api_key($google_map_api_key) ){
515 return;
516 }
517
518 wp_enqueue_script(
519 'wpfnl-google-places-api',
520 'https://maps.googleapis.com/maps/api/js?key=' . $google_map_api_key . '&libraries=places',
521 array(),
522 WPFNL_VERSION,
523 true
524 );
525
526 wp_enqueue_script(
527 'wpfnl-google-places',
528 WPFNL_DIR_URL.'public/assets/js/google-places-auto-complete.js',
529 array( 'wpfnl-google-places-api' ),
530 WPFNL_VERSION,
531 true
532 );
533
534 }
535
536
537 /**
538 * Enable checkout hook
539 */
540 private function enable_checkout_hook(){
541
542 if ( Wpfnl_functions::check_if_this_is_step_type('checkout') ) {
543
544 if( is_plugin_active( 'woocommerce-german-market/WooCommerce-German-Market.php' )){
545 add_action( 'woocommerce_checkout_order_review','woocommerce_checkout_payment' );
546 }
547 }
548 }
549
550
551 /**
552 * Save checkout metas
553 *
554 * @param $order_id
555 * @param $posted
556 *
557 * @since 1.0.0
558 */
559 public function save_checkout_fields( $order_id, $posted )
560 {
561 if ( ! isset( $_POST['_wpfunnels_checkout_id'] ) || ! Wpfnl_functions::is_wc_active() ) {
562 return;
563 }
564
565 $checkout_id = sanitize_text_field( wp_unslash( $_POST['_wpfunnels_checkout_id'] ) );
566 $funnel_id = isset( $_POST['_wpfunnels_funnel_id'] ) ? sanitize_text_field( wp_unslash( $_POST['_wpfunnels_funnel_id'] ) ) : '';
567
568 // Load order once for all meta operations.
569 $order = wc_get_order( $order_id );
570 if ( ! $order || is_wp_error( $order ) ) {
571 return;
572 }
573
574 $order->update_meta_data( '_wpfunnels_checkout_id', wc_clean( $checkout_id ) );
575
576 if ( $funnel_id ) {
577 $order->update_meta_data( '_wpfunnels_funnel_id', $funnel_id );
578 $order->update_meta_data( '_wpfunnels_order', 'yes' );
579 }
580
581 if ( isset( $_POST['_wpfunnels_order_unique_identifier'] ) ) {
582 $order->update_meta_data( '_wpfunnels_order_unique_identifier', wc_clean( sanitize_text_field( wp_unslash( $_POST['_wpfunnels_order_unique_identifier'] ) ) ) );
583 }
584
585 // Process order bump products using the already-loaded order object.
586 $order_bump_products = get_post_meta( $checkout_id, 'order-bump-settings', true );
587 if ( is_array( $order_bump_products ) ) {
588 $ob_products = [];
589 foreach ( $order_bump_products as $key => $orderBumpProduct ) {
590 if ( ! isset( $_POST[ '_wpfunnels_order_bump_product_' . $key ] ) ) {
591 continue;
592 }
593 $ob_product_id = wc_clean( sanitize_text_field( wp_unslash( $_POST[ '_wpfunnels_order_bump_product_' . $key ] ) ) );
594 $order->update_meta_data( '_wpfunnels_order_bump_product', $ob_product_id );
595 foreach ( $order->get_items() as $order_item_id => $order_item ) {
596 $product_id = ! empty( $order_item['variation_id'] ) ? $order_item['variation_id'] : $order_item['product_id'];
597 if ( $ob_product_id == $product_id ) {
598 $ob_products[] = $ob_product_id;
599 wc_add_order_item_meta( $order_item_id, '_wpfunnels_order_bump', 'yes' );
600 }
601 }
602 }
603 if ( ! empty( $ob_products ) ) {
604 $order->update_meta_data( '_wpfunnels_order_bump_products', $ob_products );
605 }
606 }
607
608 // Single save for all meta accumulated above.
609 $order->save();
610
611 if ( $funnel_id ) {
612 $helper = CheckoutHelper::getInstance();
613 $discount_instance = new WpfnlDiscount();
614 $is_time_bound = $discount_instance->maybe_time_bound_discount( $checkout_id );
615
616 if ( ! $is_time_bound || $discount_instance->maybe_validate_discount_time( $checkout_id ) ) {
617 $discount = $discount_instance->get_discount_settings( $checkout_id );
618 $main_products = $helper->get_main_products( $checkout_id, $funnel_id );
619
620 $variable_product_check = $helper->variable_product_check( $order->get_items(), $main_products, false );
621 $main_products = isset( $variable_product_check['main_products'] ) ? $variable_product_check['main_products'] : $main_products;
622 $is_main_product_in_cart = isset( $variable_product_check['is_main_product_in_cart'] ) ? $variable_product_check['is_main_product_in_cart'] : false;
623 $is_regular = isset( $discount['discountOptions'], $discount['discountapplyto'] )
624 && 'original' !== $discount['discountOptions']
625 && 'sale' !== $discount['discountapplyto'];
626
627 $total = $helper->get_custom_subtotal( $is_regular );
628 $helper->apply_discount_and_update_total( $order, $total, $discount, $is_main_product_in_cart );
629 }
630
631 $user_id = $order->get_user_id();
632 $orders[ $checkout_id ] = $order_id;
633 WC()->session->set( 'wpfnl_orders_' . $user_id . '_' . $funnel_id, $orders );
634
635 Wpfnl_functions::mark_order_owned_by_session( $order_id );
636
637 /**
638 * Fires after a funnel order is placed.
639 *
640 * @since 1.0.0
641 *
642 * @param int $order_id The ID of the order.
643 * @param string $funnel_id The ID of the funnel.
644 * @param string $checkout_id The checkout step ID associated with the order.
645 */
646 do_action( 'wpfunnels/funnel_order_placed', $order_id, $funnel_id, $checkout_id );
647 }
648 }
649
650
651 /**
652 * Automatically add products to funnel checkout
653 *
654 * @throws \Exception
655 *
656 * @since 1.0.0
657 */
658 public function initialize_cart_data() {
659 global $post;
660 if ( isset($_REQUEST['wc-ajax']) && ( 'apply_coupon' === $_REQUEST['wc-ajax'] || 'wc_stripe_get_cart_details' === $_REQUEST['wc-ajax'] ) ) {
661 return;
662 }
663
664 if(
665 is_admin()
666 || isset( $_GET[ 'removed_item' ] )
667 || !$post
668 ) {
669 return;
670 }
671
672 if(!Wpfnl_functions::is_wc_active() ){
673 return;
674 }
675
676 $checkout_id = '';
677 if( wp_doing_ajax() ){
678 $checkout_id = Wpfnl_functions::get_checkout_id_from_post($_POST);
679 }
680
681 $checkout_id = !$checkout_id ? $post->ID : $checkout_id;
682
683 // Returns if the post id is not funnel checkout type.
684 if(
685 !Wpfnl_functions::check_if_this_is_step_type_by_id( $checkout_id, 'checkout' )
686 || !Wpfnl_functions::check_if_this_is_step_type( 'checkout' )
687 || Wpfnl_functions::is_orderbump_clicked_from_post_data()
688 || Wpfnl_functions::is_checkoutify_orderbump_clicked_from_post_data()
689 || Wpfnl_functions::is_product_option_from_post_data()
690 || Wpfnl_functions::is_variation_selected_from_post_data()
691 || Wpfnl_functions::maybe_select_quantity_from_post_data()
692 || Wpfnl_functions::is_order_bump_already_accepted_from_cookie()
693 // || Wpfnl_functions::is_coupon_applied_from_post_data()
694 ) {
695 return;
696 }
697
698
699 if( isset($_REQUEST['wc-ajax']) && Wpfnl_functions::is_wc_active() && !WC()->cart->is_empty() && WC()->cart->has_discount()){
700 return;
701 }
702
703 $funnel_id = get_post_meta( $checkout_id, '_funnel_id', true );
704
705 // Returns in case of invalid funnel id or if the funnel id is `lms` type.
706 if( !$funnel_id || 'lms' === get_post_meta( $funnel_id, '_wpfnl_funnel_type', true ) ) {
707 return;
708 }
709
710 $this->render_express_checkout( $checkout_id );
711
712 /**
713 * Filter hook to modify funnel products
714 *
715 * @param array $product_array Products selected for funnel checkout.
716 * @param string|int $funnel_id Funnel ID.
717 * @param string|int $checkout_id Checkout ID.
718 *
719 * @since 2.7.11
720 */
721 $product_array = apply_filters(
722 'wpfunnels/funnel_products',
723 get_post_meta( $checkout_id, '_wpfnl_checkout_products', true ),
724 $funnel_id,
725 $checkout_id
726 );
727
728 /**
729 * This line of code applies a filter to determine if initiating a funnel checkout is allowed.
730 *
731 * @param bool $is_allow The current value indicating if initiating a funnel checkout is allowed.
732 * @param int $funnel_id The ID of the funnel being checked.
733 * @return bool The filtered value indicating if initiating a funnel checkout is allowed.
734 *
735 * @since 3.3.2
736 */
737 $is_allow = apply_filters('wpfunnels/allow_initiate_funnel_checkout', true, $funnel_id );
738 if( !$is_allow ){
739 return;
740 }
741
742 $is_store_checkout = 'store_checkout' === get_post_meta( $funnel_id, '_wpfnl_funnel_type', true );
743
744 if( !$is_store_checkout ) {
745 \WC()->cart->empty_cart();
746 }
747
748 if( !$is_store_checkout && ( !is_array( $product_array ) || empty( $product_array[ 0 ][ 'id' ] ) )) {
749 wc_clear_notices();
750 wc_add_notice( __( 'No product is added to the funnel. Please add product from checkout step settings.', 'wpfnl' ), 'error' );
751 return;
752 }
753 $price_type = $this->get_product_price_type( $funnel_id, $checkout_id );
754
755 if( $product_array && is_array( $product_array )) {
756 foreach( $product_array as $product ) {
757 if( isset( $product[ "id" ] ) && isset( $product[ "quantity" ] ) && ( !isset($product['funnel_id']) || (isset($product['funnel_id']) && (int)$funnel_id === (int)$product['funnel_id']) ) ) {
758 $product_id = !empty($product['variation_id']) ? $product[ "variation_id" ] : $product[ "id" ];
759 $quantity = $product[ "quantity" ];
760 $_product = $product_id ? wc_get_product( $product_id ) : null;
761
762 if( $_product instanceof \WC_Product ) {
763 //check if product type is variable or not from $_product object.
764
765 if( 'variable' === $_product->get_type() || 'variable-subscription' === $_product->get_type() ) {
766 $this->add_default_variation( $product_id, $_product, $quantity, $price_type );
767 continue;
768 }
769
770 $product_type = $_product->get_type();
771
772 if( 'variation' === $product_type || 'subscription_variation' === $product_type ) {
773 $is_perfect_variation = Wpfnl_functions::is_perfect_variations( $product_id );
774 $type = empty($is_perfect_variation['status']) ? 'single-variation' : 'variation';
775 $this->add_variaiton_product_in_cart( $product_id, $quantity, $price_type, $type, [], null );
776 continue;
777 }
778
779 $product_object = wc_get_product( $product_id );
780
781 if ( is_a( $product_object, 'WC_Product_Bundle' ) ) {
782
783 if( $price_type == 'original' || $price_type == 'sale' ){
784 $custom_data = [
785 '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' ),
786 'product_type' => $product_type
787 ];
788 }else{
789 $custom_data = [
790 'custom_price' => $product_object->get_bundle_regular_price( 'min' ) ? $product_object->get_bundle_regular_price( 'min' ) : $product_object->get_bundle_price('min'),
791 'product_type' => $product_type
792 ];
793 }
794 $is_bundle_exist = true;
795
796 }else{
797 $is_bundle_exist = false;
798 if( $price_type == 'original' || $price_type == 'sale' ){
799 $custom_data = [
800 'custom_price' => $product_object->is_on_sale() ? $product_object->get_sale_price() : $product_object->get_regular_price(),
801 'product_type' => $product_type
802 ];
803 }else{
804 $custom_data = [
805 '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),
806 'product_type' => $product_type
807 ];
808 }
809
810 }
811
812
813
814 /**
815 * Apply filters to modify the product price data.
816 *
817 * @param float $custom_price The custom price of the product.
818 * @return float The modified custom price.
819 *
820 * @since 3.1.2
821 */
822
823 $custom_data['custom_price'] = apply_filters( 'wpfunnels/modify_main_product_price_data', $custom_data['custom_price'] );
824 if( (wp_doing_ajax() && $is_bundle_exist) ){
825 $custom_data = [];
826 }
827 if( 'composite' === $product_object->get_type() ){
828 $is_regular = $price_type == 'original' || $price_type == 'sale' ? false : true;
829 Wpfnl_functions::add_composite_product_to_cart_with_defaults( $product_id, $quantity,$is_regular );
830 }else{
831 WC()->cart->add_to_cart( $product_id, $quantity, '' , array(), $custom_data );
832 }
833
834 }
835 }
836 }
837 }
838
839 //Add coupons for funnel checkout.
840 $this->add_coupons( $checkout_id );
841
842 /**
843 * After initialize funnels checkout
844 *
845 * This hook will trigger in checkout page
846 *
847 * @param init $funnel_id Funnel ID
848 *
849 * @since 2.7.19
850 */
851 do_action( 'wpfunnels/after_init_checkout', $funnel_id );
852 }
853
854
855 public function add_bundle_product_in_cart( $product_id,$quantity, $custom_data , $bundled_items_data ){
856 /**
857 * Apply filters to modify the product price data.
858 *
859 * @param float $custom_price The custom price of the product.
860 * @return float The modified custom price.
861 *
862 * @since 3.1.2
863 */
864 $custom_data['custom_price'] = apply_filters( 'wpfunnels/modify_main_product_price_data', $custom_data['custom_price'] );
865
866
867 WC()->cart->add_to_cart( $product_id, $quantity, '' , array(), $custom_data, $bundled_items_data );
868 }
869
870
871 /**
872 * Checks if a variation is incomplete based on its attributes.
873 *
874 * @param int $variation_id The ID of the variation.
875 * @param \WC_Product $parent_product (Optional) The parent product object. If not provided, it will be retrieved based on the variation ID.
876 *
877 * @return bool True if the variation is incomplete, false otherwise.
878 */
879 private function is_variation_incomplete( $variation_id, $parent_product = null ) {
880 if( !$parent_product ) {
881 $parent_product = wc_get_product( $variation_id );
882 }
883
884 $var_product = wc_get_product( $variation_id );
885 $attributes = $parent_product->get_attributes();
886 $var_attributes = $var_product->get_attributes();
887
888 return sizeof( $attributes ) !== sizeof( array_filter( $var_attributes ) );
889 }
890
891
892 /**
893 * Adds coupons to the funnel checkout
894 *
895 * @param int|string $checkout_id Funnel checkout ID.
896 *
897 * @since 2.7.11
898 */
899 private function add_coupons( $checkout_id ) {
900 $coupon = get_post_meta( $checkout_id, '_wpfnl_checkout_discount', true );
901 if( is_array( $coupon ) && !empty( $coupon ) ) {
902 $coupon = reset( $coupon );
903 }
904 WC()->session->set( 'order_bump_accepted', 'no' );
905
906 if( !empty( $coupon ) ) {
907 WC()->cart->add_discount( $coupon );
908 }
909 }
910
911 /**
912 * Renders express funnel checkout
913 *
914 * @param int|string $checkout_id Funnel checkout ID.
915 *
916 * @since 2.7.11
917 */
918 private function render_express_checkout( $checkout_id ) {
919 // Only for express checkout
920 if( \WPFunnels\Wpfnl_functions::is_wpfnl_pro_activated() && Wpfnl_functions::maybe_express_checkout( $checkout_id ) ) {
921 remove_action( 'woocommerce_checkout_order_review', 'woocommerce_order_review' );
922 add_filter( 'woocommerce_update_order_review_fragments', [ $this, 'wpfnl_express_checkout_shipping_table_update' ], 10, 1 );
923 }
924 }
925
926 /**
927 * Get the first id of available product variations by parent id
928 *
929 * @param int|string $parent_id Parent product id.
930 *
931 * @return int|string|null
932 * @since 2.7.11
933 */
934 private function get_first_variation_id( $parent_id ) {
935 $product_id = get_posts( [
936 'post_parent' => $parent_id,
937 'fields' => 'ids',
938 'post_type' => 'product_variation',
939 'post_status' => [ 'private', 'publish' ],
940 'numberposts' => 1,
941 'orderby' => 'menu_order',
942 'order' => 'asc'
943 ] );
944 return !empty( $product_id[ 0 ] ) ? $product_id[ 0 ] : null;
945 }
946 /**
947 * Add default variation to funnel checkout
948 *
949 * @param string|int $product_id Variable parent ID.
950 * @param \WC_Product $product Variable parent product.
951 * @param string|int $quantity Product quantity.
952 *
953 * @return void
954 * @since 2.7.11
955 * @throws \Exception
956 */
957 private function add_default_variation( $product_id, \WC_Product $product, $quantity, $price_type ) {
958 $attributes = $product->get_attributes();
959 $variation_id = null;
960 $default_variation = $product->get_default_attributes();
961 $formatted_variation = [];
962
963 if( sizeof( $attributes ) === sizeof( $default_variation ) ) {
964 $formatted_variation = $this->get_formatted_variation( $default_variation );
965 if( !empty( $formatted_variation ) ) {
966 $variation_id = ( new \WC_Product_Data_Store_CPT() )->find_matching_product_variation(
967 $product,
968 $formatted_variation
969 );
970 }
971 }
972 else {
973 $variation_id = $this->get_first_variation_id( $product_id );
974 }
975
976 if( $variation_id ) {
977 $this->add_variaiton_product_in_cart( $variation_id, $quantity, $price_type, 'variable', $formatted_variation, $product);
978 }
979 }
980
981 /**
982 * Adds a variation product to the cart.
983 *
984 * @param int $variation_id The ID of the variation.
985 * @param int $quantity The quantity of the product to add to the cart.
986 * @param array $formatted_variation The formatted variation array with attribute keys and values.
987 * @param \WC_Product $parent_product (Optional) The parent product object. If not provided, it will be retrieved based on the variation ID.
988 *
989 * @return void
990 * @since 2.7.11
991 */
992 private function add_variaiton_product_in_cart( $variation_id, $quantity, $price_type, $product_type, $formatted_variation = [], $parent_product = null ) {
993 if( !$quantity || !$variation_id ) {
994 return;
995 }
996
997 $variation_object = wc_get_product( $variation_id );
998 $parent_id = $variation_object->get_parent_id();
999 if( !$parent_product ) {
1000 $parent_product = wc_get_product( $parent_id );
1001 }
1002
1003 if( empty( $formatted_variation ) ) {
1004 $is_var_incomplete = $this->is_variation_incomplete( $variation_id, $parent_product );
1005 $formatted_variation = $is_var_incomplete ? $this->get_complete_variation_format( $variation_id, $parent_product ) : $formatted_variation;
1006 }
1007 $custom_data = [];
1008 if( $price_type == 'original' || $price_type == 'sale' ){
1009 $custom_data = [
1010 'custom_price' => $variation_object->is_on_sale() ? $variation_object->get_sale_price() : $variation_object->get_regular_price(),
1011 'product_type' => $product_type
1012 ];
1013 }else{
1014 $custom_data = [
1015 '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),
1016 'product_type' => $product_type
1017 ];
1018
1019 }
1020
1021 /**
1022 * Apply filters to modify the product price data.
1023 *
1024 * @param float $custom_price The custom price of the product.
1025 * @return float The modified custom price.
1026 *
1027 * @since 3.1.2
1028 */
1029 $custom_data['custom_price'] = apply_filters( 'wpfunnels/modify_main_product_price_data', $custom_data['custom_price'] );
1030
1031 WC()->cart->add_to_cart( $parent_id, $quantity, $variation_id, $formatted_variation, $custom_data );
1032 }
1033
1034
1035
1036 /**
1037 * Formats the variation attributes into a formatted variation array.
1038 *
1039 * @param array $var_attributes The variation attributes.
1040 *
1041 * @return array The formatted variation array with attribute keys and values.
1042 * @since 2.7.11
1043 */
1044 private function get_formatted_variation( array $var_attributes ) {
1045 $formatted_variation = [];
1046
1047 foreach( $var_attributes as $key => $value ) {
1048 $formatted_variation[ "attribute_{$key}" ] = $value;
1049 }
1050 return $formatted_variation;
1051 }
1052
1053 /**
1054 * Retrieves the complete variation format for a given variation ID and parent product.
1055 *
1056 * @param int $variation_id The variation ID.
1057 * @param \WC_Product $parent_product (Optional) The parent product object. If not provided, it will be retrieved based on the variation ID.
1058 *
1059 * @return array The formatted variation array with attribute keys and values.
1060 * @since 2.7.11
1061 */
1062 private function get_complete_variation_format( $variation_id, $parent_product = null ) {
1063 if( !$variation_id ) {
1064 return [];
1065 }
1066 if( !$parent_product ) {
1067 $parent_product = wc_get_product( $variation_id );
1068 }
1069 $var_product = wc_get_product( $variation_id );
1070 $var_attributes = $var_product instanceof \WC_Product ? $var_product->get_attributes() : [];
1071 $formatted_variation = [];
1072
1073 foreach( $var_attributes as $key => $value ) {
1074 $value = $value ?: strtolower( $this->get_first_attribute_value( $key, $parent_product ) );
1075 $formatted_variation[ "attribute_{$key}" ] = $value;
1076 }
1077 return $formatted_variation;
1078 }
1079
1080 /**
1081 * Retrieves the first value of a specified attribute from a WooCommerce product.
1082 *
1083 * @param string $key The attribute key.
1084 * @param \WC_Product $product The WooCommerce product object.
1085 *
1086 * @return string The first attribute value, or an empty string if not found.
1087 * @since 2.7.11
1088 */
1089 private function get_first_attribute_value( $key, \WC_Product $product ) {
1090 $attribute = $product->get_attribute( $key );
1091 if( strpos( $attribute, '|' ) ) {
1092 $attribute = explode( '|', $attribute );
1093 }
1094 else {
1095 $attribute = explode( ',', $attribute );
1096 }
1097 return !empty( $attribute[ 0 ] ) ? trim( $attribute[ 0 ] ) : '';
1098 }
1099
1100 /*============*/
1101
1102 /**
1103 * Update shiiping markup for express checkout
1104 *
1105 * @since 2.4.18
1106 */
1107 public function wpfnl_express_checkout_shipping_table_update( $fragments ) {
1108 ob_start();
1109 require WPFNL_DIR.'/woocommerce/templates/checkout/review-order.php';
1110 $woocommerce_shipping_methods = ob_get_clean();
1111 $fragments['.woocommerce-checkout-review-order-table'] = $woocommerce_shipping_methods;
1112 return $fragments;
1113 }
1114
1115 /**
1116 * Render content for wpfunnels_checkout shortcode
1117 *
1118 * @param $atts
1119 *
1120 * @return string
1121 *
1122 * @since 2.0.3
1123 */
1124 public function render_checkout_shortcode($atts)
1125 {
1126 $atts = shortcode_atts(
1127 array(
1128 'id' => 0,
1129 ),
1130 $atts
1131 );
1132 $checkout_id = intval($atts['id']);
1133 if (is_admin()) {
1134 if (0 === $checkout_id && isset($_POST['id'])) {
1135 $checkout_id = intval($_POST['id']);
1136 }
1137 }
1138
1139 if (empty($checkout_id)) {
1140 global $post;
1141 if(is_object($post)) {
1142 $checkout_id = intval($post->ID);
1143 }
1144 }
1145 if(!$checkout_id) {
1146 return '';
1147 }
1148 $checkout_layout = Wpfnl_functions::get_checkout_layout($checkout_id);
1149
1150 $floating_label = Wpfnl_functions::get_checkout_step_setting($checkout_id, 'wpfnl_floating_label', '');
1151
1152 $output = '';
1153 ob_start();
1154
1155 do_action('wpfunnels/before_gb_checkout_form', $checkout_id);
1156
1157 $template_file = WPFNL_DIR . "public/modules/checkout/templates/checkout-template.php";
1158 include $template_file;
1159
1160 do_action('wpfunnels/after_checkout_form', $checkout_id);
1161
1162 $output .= ob_get_clean();
1163 return $output;
1164
1165 }
1166
1167
1168 public function custom_price_to_cart_item($cart)
1169 {
1170 if ( wp_doing_ajax() && !WC()->session->__isset('reload_checkout')) {
1171 foreach ($cart->cart_contents as $key => $value) {
1172 if (isset($value['custom_price'])) {
1173 /**
1174 * Filter the custom price applied to a funnel cart item during the
1175 * update_order_review AJAX refresh.
1176 *
1177 * This callback overrides the cart item price at a very late priority
1178 * (9999), i.e. after multi-currency switchers have already converted it.
1179 * Compatibility layers can hook here to adjust the value, or return
1180 * `false`/`null` to skip the override entirely and keep the converted price.
1181 *
1182 * @param float $custom_price The stored custom price for the cart item.
1183 * @param array $value The cart item.
1184 * @param object $cart The cart object.
1185 *
1186 * @since 3.12.7
1187 */
1188 $custom_price = apply_filters( 'wpfunnels/checkout_ajax_custom_price', floatval($value['custom_price']), $value, $cart );
1189
1190 if ( false === $custom_price || null === $custom_price ) {
1191 continue;
1192 }
1193
1194 $value['data']->set_price(floatval($custom_price));
1195 }
1196 }
1197 }
1198 }
1199
1200 /**
1201 * Auto apply coupon on funnel checkout
1202 *
1203 * @param object $cart_object
1204 *
1205 * @since 2.7.9
1206 * @return void
1207 */
1208 public function apply_auto_coupon($cart_object)
1209 {
1210 global $post;
1211 if( !$post ){
1212 return false;
1213 }
1214 $checkout_id = '';
1215 if ( wp_doing_ajax() || isset( $_GET['wc-ajax'] ) ) {
1216 $checkout_id = Wpfnl_functions::get_checkout_id_from_post($_POST);
1217 } else {
1218 $checkout_id = $post->ID;
1219 }
1220 if( $checkout_id ){
1221 $coupon = Wpfnl_functions::get_applied_coupon( $checkout_id );
1222 if( $coupon ){
1223 if ( WC()->cart->has_discount( $coupon ) ) return;
1224 WC()->cart->apply_coupon( $coupon );
1225 add_action( 'woocommerce_after_order_notes', [$this, 'add_coupon_field']);
1226 wc_print_notices();
1227 }
1228 }
1229
1230 }
1231
1232 public function render_order_bump_popup()
1233 {
1234 $step_id = get_the_ID();
1235 $order_bump = get_post_meta($step_id, 'order-bump', true);
1236 $order_bump_settings = get_post_meta($step_id, 'order-bump-settings', true);
1237 require_once WPFNL_DIR . 'public/modules/checkout/templates-style/order-bump-template-style3.php';
1238 }
1239
1240
1241
1242 /**
1243 * Insert helper hidden ids
1244 *
1245 * @since 1.0.0
1246 */
1247 public function checkout_shortcode_metas()
1248 {
1249 if ($this->checkout_metas_rendered) {
1250 return;
1251 }
1252
1253 if (Wpfnl_functions::check_if_this_is_step_type('checkout')) {
1254 global $post;
1255 $this->checkout_metas_rendered = true;
1256 $this->step_id = $post->ID;
1257 $step = new Wpfnl_Steps_Store_Data();
1258 $step->read($this->step_id);
1259 $this->funnel_id = $step->get_funnel_id();
1260 echo '<input type="hidden" class="input-hidden _wpfunnels_funnel_id" name="_wpfunnels_funnel_id" value="' . intval($this->funnel_id) . '">';
1261 echo '<input type="hidden" class="input-hidden _wpfunnels_checkout_id" name="_wpfunnels_checkout_id" value="' . intval($post->ID) . '">';
1262 echo '<input type="hidden" class="input-hidden _wpfunnels_order_unique_identifier" name="_wpfunnels_order_unique_identifier" value="' . uniqid() . '">';
1263 echo '<input type="hidden" class="input-hidden _wpfunnels_select_quantity" name="_wpfunnels_select_quantity" value="no">';
1264 }
1265 }
1266
1267
1268 /**
1269 * Print the funnel hidden fields when the shipping section is skipped.
1270 *
1271 * The checkout layouts only run `woocommerce_checkout_shipping` when the cart
1272 * needs a shipping address, so a cart holding only virtual/downloadable
1273 * products never renders form-shipping.php and `woocommerce_after_order_notes`
1274 * never fires. Without the hidden fields the placed order gets no funnel meta,
1275 * and the customer lands on the WooCommerce order-received page instead of the
1276 * funnel thankyou step.
1277 *
1278 * @since 3.13.2
1279 */
1280 public function maybe_render_checkout_metas()
1281 {
1282 $this->checkout_shortcode_metas();
1283 }
1284
1285
1286 /**
1287 * Record that the additional information block has rendered.
1288 *
1289 * Hooked as early as possible on `woocommerce_before_order_notes` so the
1290 * fallback below knows form-shipping.php already produced the section.
1291 *
1292 * @since 3.13.2
1293 */
1294 public function mark_additional_fields_rendered()
1295 {
1296 $this->additional_fields_rendered = true;
1297 }
1298
1299
1300 /**
1301 * Render the additional information (order notes) block.
1302 *
1303 * WooCommerce ships this block inside form-shipping.php, which the checkout
1304 * layouts only render when the cart needs a shipping address — so a cart of
1305 * virtual/downloadable products lost the order notes field along with the
1306 * `woocommerce_before_order_notes` / `woocommerce_after_order_notes` hooks that
1307 * custom checkout fields rely on, and on shippable carts the block landed above
1308 * the "Use same address for billing" toggle. Rendering it here, on
1309 * `woocommerce_checkout_after_customer_details`, puts it after both address
1310 * forms for every cart.
1311 *
1312 * @since 3.13.2
1313 */
1314 public function maybe_render_additional_fields()
1315 {
1316 if ( $this->additional_fields_rendered ) {
1317 return;
1318 }
1319
1320 if ( ! Wpfnl_functions::check_if_this_is_step_type( 'checkout' ) ) {
1321 return;
1322 }
1323
1324 if ( ! Wpfnl_functions::is_wc_active() || ! function_exists( 'WC' ) ) {
1325 return;
1326 }
1327
1328 $checkout = WC()->checkout();
1329 if ( ! $checkout ) {
1330 return;
1331 }
1332
1333 $this->additional_fields_rendered = true;
1334 ?>
1335 <div class="woocommerce-additional-fields">
1336 <?php do_action( 'woocommerce_before_order_notes', $checkout ); ?>
1337
1338 <?php if ( apply_filters( 'woocommerce_enable_order_notes_field', 'yes' === get_option( 'woocommerce_enable_order_comments', 'yes' ) ) ) : ?>
1339 <h3><?php esc_html_e( 'Additional information', 'woocommerce' ); ?></h3>
1340
1341 <div class="woocommerce-additional-fields__field-wrapper">
1342 <?php foreach ( $checkout->get_checkout_fields( 'order' ) as $key => $field ) : ?>
1343 <?php woocommerce_form_field( $key, $field, $checkout->get_value( $key ) ); ?>
1344 <?php endforeach; ?>
1345 </div>
1346 <?php endif; ?>
1347
1348 <?php do_action( 'woocommerce_after_order_notes', $checkout ); ?>
1349 </div>
1350 <?php
1351 }
1352
1353
1354 /**
1355 * Insert helper hidden ids
1356 *
1357 * @since 1.0.0
1358 */
1359 public function add_coupon_field()
1360 {
1361 if (Wpfnl_functions::check_if_this_is_step_type('checkout')) {
1362 echo '<input type="hidden" class="input-hidden _wpfunnels_funnel_id" name="_wpfunnels_auto_coupon" value="yes">';
1363 }
1364 }
1365
1366
1367 /**
1368 * Next step button ajax
1369 *
1370 * @since 1.0.0
1371 */
1372 public function wpfnl_next_button_ajax()
1373 {
1374
1375 $step_id = sanitize_text_field($_POST['step_id']);
1376 $funnel_id = get_post_meta($step_id, '_funnel_id', true);
1377 $parent_step_id = get_post_meta($step_id, '_parent_step_id', true);
1378 $step_id= $parent_step_id ? $parent_step_id : $step_id;
1379 $next_step = Wpfnl_functions::get_next_conditional_step( $funnel_id, $step_id );
1380 $url = isset($_POST['url']) ? $_POST['url'] : '';
1381 $parts = parse_url($url);
1382 $query = [];
1383 if( isset($parts['query']) ){
1384 parse_str($parts['query'], $query);
1385 }
1386
1387 $next_step = apply_filters( 'wpfunnels/next_step_data', $next_step );
1388
1389 if ( $next_step ) {
1390
1391 $custom_url = Wpfnl_functions::custom_url_for_thankyou_page( $next_step['step_id'] );
1392 if( $custom_url ){
1393 $response = $custom_url;
1394 }else{
1395 $redirect_url = add_query_arg( $this->prepare_query_param($query), get_the_permalink($next_step['step_id']));
1396 if ($redirect_url) {
1397 $response = $redirect_url;
1398 } else {
1399 $response = 'error';
1400 }
1401 }
1402
1403 ob_start();
1404 do_action( 'wpfunnels/trigger_cta', $step_id, $funnel_id );
1405 ob_get_clean();
1406
1407 wp_send_json($response);
1408 }
1409 return false;
1410 }
1411
1412
1413 /**
1414 * Check if an email exists and return CartFlows-compatible response shape.
1415 *
1416 * @return void
1417 */
1418 public function wpfnl_check_email_exists() {
1419
1420 if ( isset( $_POST['security'] ) && ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['security'] ) ), 'wpfnl' ) ) {
1421 wp_send_json_error(
1422 array(
1423 'success' => false,
1424 'is_login_allowed' => false,
1425 'msg' => __( 'Invalid request.', 'wpfnl' ),
1426 )
1427 );
1428 }
1429
1430 $email = isset( $_POST['email'] ) ? sanitize_email( wp_unslash( $_POST['email'] ) ) : '';
1431
1432 if ( empty( $email ) || ! is_email( $email ) ) {
1433 wp_send_json_success(
1434 array(
1435 'success' => false,
1436 'is_login_allowed' => 'yes' === get_option( 'woocommerce_enable_checkout_login_reminder' ),
1437 'msg' => __( 'Invalid email address.', 'wpfnl' ),
1438 )
1439 );
1440 }
1441
1442 $email_exists = (bool) email_exists( $email );
1443 $is_login_allowed = 'yes' === get_option( 'woocommerce_enable_checkout_login_reminder' );
1444
1445 wp_send_json_success(
1446 array(
1447 'success' => $email_exists,
1448 'is_login_allowed' => $is_login_allowed,
1449 'msg' => $email_exists ? __( 'Email exists', 'wpfnl' ) : __( 'Email not exist', 'wpfnl' ),
1450 )
1451 );
1452 }
1453
1454
1455 /**
1456 * Prepare query param for redirect url
1457 *
1458 * @param String $name
1459 * @param Array $query
1460 *
1461 * @return Array
1462 * @since 2.5.9
1463 */
1464 private function prepare_query_param( $query ){
1465 $query_param = [];
1466 if( isset( $query['optin'], $query['uname'] ) ){
1467 $query_param['optin'] = $query['optin'];
1468 $query_param['uname'] = $query['uname'];
1469 }
1470
1471 if( isset($query['wpfnl-order'], $query['wpfnl-key']) ){
1472 $query_param['wpfnl-order'] = $query['wpfnl-order'];
1473 $query_param['wpfnl-key'] = $query['wpfnl-key'];
1474 }
1475
1476 /**
1477 * Filter to change the query parameter of the redirect URL of funnel's steps.
1478 * Returns the query parameter after applying the 'wpfunnels/prepare_query_param' filter.
1479 *
1480 * @param array $query_param
1481 * @return array $query_param
1482 *
1483 * @since 3.4.13
1484 */
1485 return apply_filters( 'wpfunnels/prepare_query_param', $query_param );
1486 }
1487
1488
1489 /**
1490 * Calculate Discount.
1491 *
1492 * @since 1.1.5
1493 */
1494 public function calculate_custom_price($discount_type, $discount_value, $product_price)
1495 {
1496 $custom_price = $product_price;
1497
1498 if (!empty($discount_type)) {
1499 if ('discount-percentage' === $discount_type) {
1500 if ( $discount_value > 0 && $discount_value <= 100) {
1501 $custom_price = $product_price - (($product_price * $discount_value) / 100);
1502 }else{
1503 $custom_price = $product_price;
1504 }
1505 } elseif ('discount-price' === $discount_type) {
1506 if ($discount_value > 0 && $product_price >= $discount_value ) {
1507 $custom_price = $product_price - $discount_value;
1508 }else{
1509 $custom_price = $product_price;
1510 }
1511 }
1512 }
1513
1514 return number_format($custom_price, 2);
1515 }
1516
1517 public function wpfnl_update_variation(){
1518
1519 $variations = $_POST['variations'];
1520 $product_id = $_POST['variations'][0]['product_id'];
1521 $variation_id = $_POST['variations'][0]['variation_id'];
1522 $quantity = $_POST['variations'][0]['quantity'];
1523
1524 $_product = wc_get_product($variation_id);
1525 if( $_product ){
1526 $attribute = $_product->get_attributes();
1527 foreach ( $attribute as $key => $value) {
1528 if( $value ){
1529 $formatted_variation['attribute_'.$key] = $value;
1530 }else{
1531 foreach( $variations as $variation ){
1532 if( $variation['attr'] == $key ){
1533 $formatted_variation['attribute_'.$key] = $variation['value'];
1534 }
1535 }
1536 }
1537 }
1538 }
1539
1540
1541 $variation_id = (new \WC_Product_Data_Store_CPT())->find_matching_product_variation(
1542 new \WC_Product($product_id),
1543 $formatted_variation
1544 );
1545
1546 foreach ( WC()->cart->get_cart() as $cart_item_key => $cart_item ) {
1547 if( !isset($cart_item['product_type'])|| (isset($cart_item['product_type']) && 'single-variation' === $cart_item['product_type']) ){
1548 if ($cart_item['product_id'] == $product_id) {
1549 WC()->cart->remove_cart_item($cart_item_key);
1550 }
1551 }
1552 }
1553 $cart_item_data = [
1554 'custom_price' => get_post_meta($variation_id, '_price', true) ? get_post_meta($variation_id, '_price', true) : get_post_meta($variation_id, '_regular_price', true),
1555 'product_type' => 'single-variation'
1556 ];
1557
1558 WC()->cart->add_to_cart( $product_id, $quantity, $variation_id, $formatted_variation, $cart_item_data);
1559 }
1560
1561
1562
1563
1564
1565 /**
1566 * Order Bump Ajax Initialized
1567 * When you click on add order bump button it will add the product to cart and click again to recover cart
1568 *
1569 * @since 1.1.
1570 */
1571 public function wpfnl_get_course_details()
1572 {
1573 $course_id = filter_input(INPUT_POST, 'course_id', FILTER_VALIDATE_INT);
1574 $course_meta = Wpfnl_lms_learndash_functions::get_course_details_by_id($course_id);
1575 // ld_update_course_access( 1, $course_id, false );
1576 $data = [
1577 'status' => 'success',
1578 'course' => $course_meta,
1579 ];
1580
1581 wp_send_json( $data );
1582
1583 }
1584
1585
1586 /**
1587 * Order Bump Ajax Initialized
1588 * When you click on add order bump button it will add the product to cart and click again to recover cart
1589 *
1590 * @since 1.1.
1591 */
1592 public function wpfnl_order_bump_ajax() {
1593 if ( ! check_ajax_referer( 'wpfnl', 'security', false ) ) {
1594 wp_send_json_error( array( 'message' => __( 'Unauthorized request', 'wpfnl' ) ), 403 );
1595 }
1596
1597 $step_id = filter_input( INPUT_POST, 'step_id', FILTER_VALIDATE_INT );
1598 $product_id = filter_input( INPUT_POST, 'product', FILTER_VALIDATE_INT );
1599 $variation_id = filter_input( INPUT_POST, 'variation_id', FILTER_VALIDATE_INT );
1600 $checker = filter_input( INPUT_POST, 'checker', FILTER_SANITIZE_SPECIAL_CHARS );
1601 $quantity = filter_input( INPUT_POST, 'quantity', FILTER_SANITIZE_SPECIAL_CHARS );
1602 $key = filter_input( INPUT_POST, 'key', FILTER_SANITIZE_SPECIAL_CHARS );
1603 $is_lms = filter_input( INPUT_POST, 'is_lms', FILTER_SANITIZE_SPECIAL_CHARS );
1604 $user_id = filter_input( INPUT_POST, 'user_id', FILTER_SANITIZE_SPECIAL_CHARS );
1605 $funnel_id = Wpfnl_functions::get_funnel_id_from_step( $step_id );
1606 $type = get_post_meta( $funnel_id, '_wpfnl_funnel_type', true );
1607 $type = !$type ? 'wc' : $type;
1608
1609 // The bump's discount is pinned to the product it was configured for, and
1610 // the quantity is the merchant's. Otherwise any step's offer could be
1611 // borrowed and applied to any product.
1612 if ( 'lms' !== $type ) {
1613 $bump = Wpfnl_functions::resolve_order_bump_item( $step_id, $key, $product_id, $variation_id );
1614
1615 if ( is_wp_error( $bump ) ) {
1616 wp_send_json_error( array( 'message' => $bump->get_error_message() ), 400 );
1617 }
1618
1619 $product_id = $bump['product_id'];
1620 $variation_id = $bump['variation_id'];
1621 $quantity = $bump['quantity'];
1622 $cart_product_id = $bump['cart_product_id'];
1623 } else {
1624 // Validate quantity - must be a positive integer
1625 $quantity = absint( $quantity );
1626 if ( $quantity < 1 ) {
1627 $quantity = 1;
1628 }
1629
1630 // If variation_id exists, use it as product_id for cart functions.
1631 $cart_product_id = $variation_id ? $variation_id : $product_id;
1632 }
1633
1634 $class_object = Wpfnl_Public_Type_Factory::build( $type );
1635 if( $class_object ) {
1636 $data = $class_object->wpfnl_order_bump_trigger( $step_id, $cart_product_id, $quantity, $key, $user_id, $funnel_id, $checker );
1637 if( $type === 'lms' ) {
1638 wp_send_json( $data );
1639 }
1640 elseif( $type === 'wc' ) {
1641 add_filter( 'woocommerce_locate_template', [ new Wpfnl_Public(), 'wpfunnels_woocommerce_locate_template' ], 20, 3 );
1642 wp_send_json( Wpfnl_functions::get_wc_fragments( $data ) );
1643 }
1644 }
1645 }
1646
1647
1648 /**
1649 * Get replaceable orderbump products
1650 */
1651 private function get_replaceable_ob_products( $ob_settings ){
1652
1653
1654 $replaceable_ob = [];
1655 foreach( $ob_settings as $key=>$settings ){
1656 if( $settings['isReplace'] === 'yes' ){
1657 $replaceable_ob[] = [
1658 'id' => $settings['product'],
1659 'quantity' => $settings['quantity'],
1660 ];
1661 }
1662 }
1663
1664 return $replaceable_ob;
1665
1666 }
1667
1668
1669 /**
1670 * Get calculable price
1671 *
1672 * @param \WC_Product $product
1673 * @param $discount_apply
1674 *
1675 * @return string
1676 *
1677 * @since 2.0.5
1678 */
1679 private function get_product_price( \WC_Product $product, $discount_apply = 'regular' ) {
1680 $price = $product->get_regular_price();
1681 // if( is_plugin_active( 'woocommerce-subscriptions/woocommerce-subscriptions.php' ) ){
1682 // $signUpFee = \WC_Subscriptions_Product::get_sign_up_fee( $product );
1683 // $price = $price + $signUpFee;
1684 // }
1685 return $discount_apply === 'sale' && $product->get_sale_price() ? $product->get_sale_price() : $price;
1686 }
1687
1688
1689 /**
1690 * Add checkout products to cart
1691 *
1692 * @param $checkout_id
1693 *
1694 * @throws \Exception
1695 */
1696 private function add_checkout_products($checkout_id)
1697 {
1698 $checkout_products = get_post_meta( $checkout_id, '_wpfnl_checkout_products', true );
1699 foreach ($checkout_products as $product) {
1700 $_product_id = $product['id'];
1701 $_qty = $product['quantity'];
1702 WC()->cart->add_to_cart($_product_id, $_qty);
1703 }
1704 }
1705
1706 /**
1707 * Add last updated checkout products to cart
1708 *
1709 * @param $updated_cart
1710 *
1711 * @throws \Exception
1712 */
1713 private function update_checkout_with_last_updated_cart($updated_cart){
1714 foreach ($updated_cart as $cart) {
1715 WC()->cart->add_to_cart( $cart['product_id'], $cart['quantity'], $cart['variation_id'], $cart['variation'], $cart['line_total']);
1716 }
1717 }
1718
1719
1720 public function wpfnl_checkout_cart()
1721 {
1722 $values = [];
1723 parse_str(sanitize_text_field($_POST['post_data']), $values);
1724 $cart = $values['cart'];
1725 foreach ($cart as $cart_key => $cart_value) {
1726 WC()->cart->set_quantity($cart_key, $cart_value['qty'], false);
1727 WC()->cart->calculate_totals();
1728 woocommerce_cart_totals();
1729 }
1730 wp_die();
1731 }
1732
1733
1734 /**
1735 * Order Bump Templates
1736 *
1737 * @param $settings
1738 *
1739 * @since 1.0.0
1740 */
1741 public function render_order_bump_template($settings)
1742 {
1743 if (!empty($settings['selectedStyle'])) {
1744 require_once WPFNL_DIR . 'public/modules/checkout/templates-style/order-bump-template-' . $settings['selectedStyle'] . '.php';
1745 }
1746 }
1747
1748
1749 /**
1750 * Display coupon field or not
1751 *
1752 * @since 2.0.5
1753 */
1754 public function display_coupon_field() {
1755 if ( Wpfnl_functions::check_if_this_is_step_type('checkout') ) {
1756 global $post;
1757 $checkout_id = $post->ID;
1758 $show_coupon = Wpfnl::get_instance()->meta->get_checkout_meta_value( $checkout_id, '_wpfnl_checkout_coupon' );
1759 if( 'no' === $show_coupon ) {
1760 remove_action( 'woocommerce_before_checkout_form', 'woocommerce_checkout_coupon_form' );
1761 }
1762 }
1763 }
1764
1765 /**
1766 * Out of Stock message in checkout page
1767 *
1768 * @param $message
1769 * @param $product_data
1770 *
1771 * @return string
1772 */
1773
1774 public function wpfnl_out_of_stock_message_in_checkout( $message, $product_data ){
1775 $message = __( 'Sorry, the following product(s) that you are willing to purchase is out of stock at the moment.', 'wpfnl' );
1776 $image = wp_get_attachment_image_src( get_post_thumbnail_id( $product_data->get_id() ), array( 100, 100) );
1777 $message .= '<div class="wpfnl-checkout-stock-message"><img src="'.$image[0].'"><h4>'.$product_data->get_title().' </h4></div>';
1778 return $message;
1779 }
1780
1781
1782 /**
1783 * Remove Astra theme checkout shipping hook
1784 */
1785 public function remove_astra_theme_checkout_shipping_hook(){
1786 return true;
1787 }
1788
1789
1790 /**
1791 * Update quantity from checkout page
1792 *
1793 * @param $quantity, $cart_item, $cart_item_key
1794 */
1795 public function wpfnl_checkout_cart_item_quantity( $quantity, $cart_item, $cart_item_key ) {
1796
1797 $step_id = 0;
1798 $isQuantity = 'no';
1799
1800 if( wp_doing_ajax() ) {
1801 $step_id = isset($_POST['step_id']) ? $_POST['step_id'] : 0;
1802 } else {
1803 $step_id = get_the_ID();
1804 }
1805
1806
1807 $isQuantity = get_post_meta($step_id, '_wpfnl_quantity_support',true);
1808 $quantityLimit = get_post_meta($step_id, '_wpfnl_quantity_limit', true);
1809 $order_bump_product = get_post_meta($step_id,'order-bump-settings',true);
1810
1811 if($isQuantity === 'yes'){
1812 if(isset($order_bump_product['product']) && isset($order_bump_product['isEnabled'])){
1813
1814 $variations = json_encode($cart_item['variation']);
1815 $product_id = $cart_item["product_id"];
1816 $quantity = $cart_item["quantity"];
1817 $variation_id = $cart_item["variation_id"];
1818 $isQuantityLimit = false;
1819 $set_quantity = 0;
1820
1821 if( isset($quantityLimit['isEnabled']) && $quantityLimit['isEnabled'] === 'yes' ){
1822 $set_quantity = $quantityLimit['quantity'];
1823 $isQuantityLimit = true;
1824 }
1825
1826 if( $isQuantityLimit ){
1827 $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'/>";
1828 }else{
1829 $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' />";
1830 } }
1831 }
1832 return $quantity;
1833
1834 }
1835
1836
1837 public function before_checkout_form_actions( $checkout_id ) {
1838
1839 if( Wpfnl_functions::is_wc_active() ){
1840
1841 remove_all_actions( 'woocommerce_checkout_billing' );
1842 remove_all_actions( 'woocommerce_checkout_shipping' );
1843
1844 add_action( 'woocommerce_checkout_billing', array( WC()->checkout, 'checkout_form_billing' ) );
1845 add_action( 'woocommerce_checkout_shipping', array( WC()->checkout, 'checkout_form_shipping' ) );
1846 }
1847
1848 }
1849
1850
1851 /**
1852 * Render custom shipping section for modern checkout only.
1853 *
1854 * @return void
1855 */
1856 public function add_custom_shipping_section() {
1857 if ( ! $this->is_modern_checkout_layout() || ! apply_filters( 'wpfunnels_should_render_custom_shipping', true ) ) {
1858 return;
1859 }
1860 if ( ! Wpfnl_functions::is_wc_active() || ! function_exists( 'WC' ) || ! WC()->cart || ! WC()->cart->needs_shipping() ) {
1861 return;
1862 }
1863 ob_start();
1864 echo '<div class="wpfnl-customer-shipping">';
1865 $this->wpfnl_cart_totals_shipping_html();
1866 echo '</div>';
1867 ob_end_flush();
1868 }
1869
1870
1871 /**
1872 * Give the `wpfunnels/after_order_total` block its own order-review fragment.
1873 *
1874 * WooCommerce's update_order_review AJAX returns the entire review-order template
1875 * under the `.woocommerce-checkout-review-order-table` key, and checkout.js runs
1876 * `$( key ).replaceWith( value )`. That selector matches only the <table>, so the
1877 * coupon field, order bumps and testimonials that our template appends after it
1878 * were injected again on every refresh while the previous copies remained as
1879 * siblings — one extra copy per AJAX call.
1880 *
1881 * Split the buffer at the wrapper emitted by review-order.php so each key matches
1882 * the outermost node of its own value, which makes both replacements idempotent.
1883 *
1884 * If the wrapper is missing — a theme or plugin overriding review-order.php with
1885 * its own copy — leave the fragments untouched and keep the previous behaviour.
1886 *
1887 * @param array $fragments Fragments keyed by CSS selector.
1888 *
1889 * @return array
1890 * @since 3.13.2
1891 */
1892 public function split_after_order_total_fragment( $fragments ) {
1893 $table_key = '.woocommerce-checkout-review-order-table';
1894
1895 if ( ! is_array( $fragments ) || empty( $fragments[ $table_key ] ) || ! is_string( $fragments[ $table_key ] ) ) {
1896 return $fragments;
1897 }
1898
1899 $wrapper = '<div class="wpfnl-after-order-total">';
1900 // The wrapper is the last thing review-order.php prints; search from the end so
1901 // markup echoed by a hooked callback can never be mistaken for it.
1902 $position = strrpos( $fragments[ $table_key ], $wrapper );
1903
1904 if ( false === $position ) {
1905 return $fragments;
1906 }
1907
1908 $fragments['.wpfnl-after-order-total'] = substr( $fragments[ $table_key ], $position );
1909 $fragments[ $table_key ] = substr( $fragments[ $table_key ], 0, $position );
1910
1911 return $fragments;
1912 }
1913
1914
1915 /**
1916 * Update checkout fragments for customer details and multistep navigation
1917 * when cart items (or order bumps) change during update_order_review AJAX.
1918 *
1919 * @param array $fragments
1920 * @return array
1921 */
1922 public function wpfnl_update_checkout_fragments( $fragments ) {
1923 if ( ! Wpfnl_functions::is_wc_active() || ! function_exists( 'WC' ) || ! WC()->cart || ! WC()->checkout() ) {
1924 return $fragments;
1925 }
1926
1927 // These fragments re-render WPFunnels' own checkout markup. A theme or page
1928 // builder that ships its own form-checkout.php lays the region out differently
1929 // and can park the order notes block -- and anything hooked to
1930 // `woocommerce_after_order_notes`, such as a relocated payment box -- inside
1931 // #customer_details. Replacing that node with our markup deletes it on the first
1932 // update_order_review, so only rebuild what we rendered ourselves.
1933 if ( ! $this->is_wpfnl_checkout_template_request() ) {
1934 return $fragments;
1935 }
1936
1937 $checkout_id = Wpfnl_functions::get_checkout_id_from_post( $_POST );
1938 if ( ! $checkout_id && ! empty( $_POST['wpfnl_step_id'] ) ) {
1939 $checkout_id = absint( $_POST['wpfnl_step_id'] );
1940 }
1941 if ( ! $checkout_id && ! empty( $_POST['step_id'] ) ) {
1942 $checkout_id = absint( $_POST['step_id'] );
1943 }
1944 if ( ! $checkout_id && ! empty( $_POST['post_data'] ) ) {
1945 $post_data = array();
1946 wp_parse_str( wp_unslash( $_POST['post_data'] ), $post_data );
1947 if ( ! empty( $post_data['wpfnl_step_id'] ) ) {
1948 $checkout_id = absint( $post_data['wpfnl_step_id'] );
1949 } elseif ( ! empty( $post_data['_wpfunnels_checkout_id'] ) ) {
1950 $checkout_id = absint( $post_data['_wpfunnels_checkout_id'] );
1951 }
1952 }
1953
1954 if ( ! $checkout_id ) {
1955 return $fragments;
1956 }
1957
1958 // Re-initialize actions for billing and shipping form rendering
1959 add_filter( 'woocommerce_locate_template', array( new Wpfnl_Public(), 'wpfunnels_woocommerce_locate_template' ), 20, 3 );
1960 remove_all_actions( 'woocommerce_checkout_billing' );
1961 remove_all_actions( 'woocommerce_checkout_shipping' );
1962 add_action( 'woocommerce_checkout_billing', array( WC()->checkout(), 'checkout_form_billing' ) );
1963 add_action( 'woocommerce_checkout_shipping', array( WC()->checkout(), 'checkout_form_shipping' ) );
1964
1965 $checkout_layout = get_post_meta( $checkout_id, '_wpfnl_checkout_layout', true );
1966 if ( empty( $checkout_layout ) && isset( $_SESSION['checkout_layout'] ) ) {
1967 $checkout_layout = $_SESSION['checkout_layout'];
1968 }
1969
1970 // Each fragment below re-fires the checkout form hooks, so the one-shot
1971 // guard in checkout_shortcode_metas() has to be cleared before every render.
1972 // Without this only the first fragment carries _wpfunnels_funnel_id and the
1973 // placed order loses its funnel, so the buyer never reaches the offer step.
1974 $this->checkout_metas_rendered = false;
1975
1976 // 1. Render #customer_details for default, express, 2-step, modern-one-column
1977 if ( 'wpfnl-modern-one-column' === $checkout_layout ) {
1978 ob_start();
1979 ?>
1980 <div class="col2-set" id="customer_details">
1981 <?php do_action( 'wpfunnels/before_customer_details' ); ?>
1982 <?php if ( WC()->cart && WC()->cart->needs_shipping_address() ) : ?>
1983 <div class="wpfnl-modern-section wpfnl-modern-section--shipping" data-section="shipping">
1984 <div class="wpfnl-modern-section__content">
1985 <div class="col-2" id="wpfnl_checkout_shipping">
1986 <?php do_action( 'woocommerce_checkout_shipping' ); ?>
1987 </div>
1988 </div>
1989 </div>
1990 <?php endif; ?>
1991 <div class="wpfnl-modern-section wpfnl-modern-section--billing" data-section="billing">
1992 <div class="wpfnl-modern-section__content">
1993 <div class="col-1" id="wpfnl_checkout_billing">
1994 <?php do_action( 'woocommerce_checkout_billing' ); ?>
1995 </div>
1996 </div>
1997 </div>
1998 <?php do_action( 'wpfunnels/after_customer_details' ); ?>
1999 </div>
2000 <?php
2001 $fragments['#customer_details'] = ob_get_clean();
2002 } else {
2003 ob_start();
2004 ?>
2005 <div class="col2-set" id="customer_details">
2006 <?php do_action( 'wpfunnels/before_customer_details' ); ?>
2007
2008 <?php
2009 /*
2010 * The classic layouts render the contact card as the first child of
2011 * #customer_details, so this fragment has to reproduce it - replacing the node
2012 * without it would take the required email field off the page on the first
2013 * cart update. form-billing.php drops billing_email from the billing fieldset
2014 * unconditionally, so this stays the only copy of it.
2015 */
2016 $checkout = WC()->checkout();
2017 include WPFNL_DIR . 'public/modules/checkout/templates/parts/customer-information.php';
2018 ?>
2019
2020 <?php if ( WC()->cart && WC()->cart->needs_shipping_address() ) : ?>
2021 <div class="col-1" id="wpfnl_checkout_shipping">
2022 <?php do_action( 'woocommerce_checkout_shipping' ); ?>
2023 </div>
2024 <div class="col-2" id="wpfnl_checkout_billing">
2025 <?php do_action( 'woocommerce_checkout_billing' ); ?>
2026 </div>
2027 <?php else : ?>
2028 <div class="col-1" id="wpfnl_checkout_billing">
2029 <?php do_action( 'woocommerce_checkout_billing' ); ?>
2030 </div>
2031 <?php endif; ?>
2032 <?php do_action( 'wpfunnels/after_customer_details' ); ?>
2033 </div>
2034 <?php
2035 $fragments['#customer_details'] = ob_get_clean();
2036 }
2037
2038 $this->checkout_metas_rendered = false;
2039
2040 // 2. Render #wpfnl_modern_customer_details for modern 2-column and modern multistep
2041 ob_start();
2042 ?>
2043 <div id="wpfnl_modern_customer_details" class="wpfnl-modern-customer-details">
2044 <?php do_action( 'wpfunnels/before_customer_details' ); ?>
2045 <?php if ( WC()->cart && WC()->cart->needs_shipping_address() ) : ?>
2046 <?php do_action( 'woocommerce_checkout_shipping' ); ?>
2047 <?php endif; ?>
2048 <?php do_action( 'woocommerce_checkout_billing' ); ?>
2049 <?php do_action( 'wpfunnels/after_customer_details' ); ?>
2050 </div>
2051 <?php
2052 $fragments['#wpfnl_modern_customer_details'] = ob_get_clean();
2053
2054 // 3. Render .wpfnl-multistep-wizard for multistep/express/2-step layouts
2055 ob_start();
2056 if ( 'wpfnl-2-step' === $checkout_layout ) {
2057 // The 2-step layout has its own wizard shape: Login (optional) / Information / Payment.
2058 // Information covers both shipping and billing; Payment covers the order review.
2059 $two_step_billing_active_cls = is_user_logged_in() || 'no' === get_option( 'woocommerce_enable_checkout_login_reminder' ) ? 'current' : '';
2060 ?>
2061 <ul class="wpfnl-multistep-wizard">
2062 <?php if ( ! is_user_logged_in() && 'yes' === get_option( 'woocommerce_enable_checkout_login_reminder' ) ) { ?>
2063 <li class="login current">
2064 <button type="button" data-target="login">
2065 <span class="step-icon">
2066 <?php include WPFNL_DIR . 'public/assets/icons/icon-login.php'; ?>
2067 </span>
2068 <span class="step-title">
2069 <?php esc_html_e( 'Login', 'wpfnl' ); ?>
2070 </span>
2071 </button>
2072 </li>
2073 <?php } ?>
2074
2075 <li class="billing <?php echo esc_attr( $two_step_billing_active_cls ); ?>">
2076 <button type="button" class="two-step" data-target="billing">
2077 <span class="step-icon">
2078 <?php include WPFNL_DIR . 'public/assets/icons/icon-billing.php'; ?>
2079 </span>
2080 <span class="step-title">
2081 <?php esc_html_e( 'Information', 'wpfnl' ); ?>
2082 </span>
2083 </button>
2084 </li>
2085
2086 <li class="order-review">
2087 <button type="button" class="two-step" data-target="order-review">
2088 <span class="step-icon">
2089 <?php include WPFNL_DIR . 'public/assets/icons/icon-order-review.php'; ?>
2090 </span>
2091 <span class="step-title">
2092 <?php esc_html_e( 'Payment', 'wpfnl' ); ?>
2093 </span>
2094 </button>
2095 </li>
2096 </ul>
2097 <?php
2098 } else {
2099 ?>
2100 <ul class="wpfnl-multistep-wizard">
2101 <?php if ( ! is_user_logged_in() && 'yes' === get_option( 'woocommerce_enable_checkout_login_reminder' ) ) { ?>
2102 <li class="login current">
2103 <button type="button" data-target="login">
2104 <span class="step-icon">
2105 <?php include WPFNL_DIR . 'public/assets/icons/icon-login.php'; ?>
2106 </span>
2107 <span class="step-title">
2108 <?php esc_html_e( 'Login', 'wpfnl' ); ?>
2109 </span>
2110 </button>
2111 </li>
2112 <?php } ?>
2113
2114 <?php
2115 $shipping_active_cls = ( is_user_logged_in() || 'no' === get_option( 'woocommerce_enable_checkout_login_reminder' ) ) ? 'current' : '';
2116 $billing_active_cls = ( ( ! is_user_logged_in() && 'yes' === get_option( 'woocommerce_enable_checkout_login_reminder' ) ) || ( WC()->cart && ! WC()->cart->needs_shipping_address() ) ) ? 'current' : '';
2117 ?>
2118 <?php if ( is_user_logged_in() || 'no' === get_option( 'woocommerce_enable_checkout_login_reminder' ) ) { ?>
2119 <?php if ( WC()->cart && WC()->cart->needs_shipping_address() ) { ?>
2120 <li class="shipping <?php echo esc_attr( $shipping_active_cls ); ?>">
2121 <button type="button" data-target="shipping">
2122 <span class="step-icon">
2123 <?php include WPFNL_DIR . 'public/assets/icons/icon-shipping.php'; ?>
2124 </span>
2125 <span class="step-title">
2126 <?php esc_html_e( 'Shipping', 'wpfnl' ); ?>
2127 </span>
2128 </button>
2129 </li>
2130 <?php } ?>
2131 <li class="billing <?php echo esc_attr( $billing_active_cls ); ?>">
2132 <button type="button" data-target="billing">
2133 <span class="step-icon">
2134 <?php include WPFNL_DIR . 'public/assets/icons/icon-billing.php'; ?>
2135 </span>
2136 <span class="step-title">
2137 <?php esc_html_e( 'Billing', 'wpfnl' ); ?>
2138 </span>
2139 </button>
2140 </li>
2141 <li class="order-review">
2142 <button type="button" data-target="order-review">
2143 <span class="step-icon">
2144 <?php include WPFNL_DIR . 'public/assets/icons/icon-order-review.php'; ?>
2145 </span>
2146 <span class="step-title">
2147 <?php esc_html_e( 'Your Order', 'wpfnl' ); ?>
2148 </span>
2149 </button>
2150 </li>
2151 <?php } ?>
2152 </ul>
2153 <?php
2154 }
2155 $fragments['.wpfnl-multistep-wizard'] = ob_get_clean();
2156
2157 // 4. Render .wpfnl-modern-multistep-nav and .wpfnl-modern-multistep-navigation for modern multistep
2158 ob_start();
2159 ?>
2160 <nav class="wpfnl-modern-multistep-nav" aria-label="<?php esc_attr_e( 'Checkout steps', 'wpfnl' ); ?>">
2161 <ul class="wpfnl-modern-multistep-nav-steps" role="tablist" aria-orientation="horizontal">
2162 <li class="wpfnl-modern-multistep-nav-step active" role="presentation" data-step="information">
2163 <button
2164 type="button"
2165 class="wpfnl-modern-multistep-nav-step-btn"
2166 role="tab"
2167 id="wpfnl-step-tab-information"
2168 aria-controls="wpfnl-step-panel-information"
2169 aria-selected="true"
2170 data-step="information"
2171 >
2172 <?php esc_html_e( 'Information', 'wpfnl' ); ?>
2173 </button>
2174 </li>
2175
2176 <?php if ( WC()->cart && WC()->cart->needs_shipping_address() ) : ?>
2177 <li class="wpfnl-modern-multistep-nav-sep" aria-hidden="true" role="presentation">
2178 <svg width="7" height="11" viewBox="0 0 7 11" fill="none" xmlns="http://www.w3.org/2000/svg" focusable="false" aria-hidden="true">
2179 <path d="M1 1L6 5.5L1 10" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
2180 </svg>
2181 </li>
2182
2183 <li class="wpfnl-modern-multistep-nav-step" role="presentation" data-step="billing">
2184 <button
2185 type="button"
2186 class="wpfnl-modern-multistep-nav-step-btn"
2187 role="tab"
2188 id="wpfnl-step-tab-billing"
2189 aria-controls="wpfnl-step-panel-billing"
2190 aria-selected="false"
2191 data-step="billing"
2192 >
2193 <?php esc_html_e( 'Billing', 'wpfnl' ); ?>
2194 </button>
2195 </li>
2196 <?php endif; ?>
2197
2198 <li class="wpfnl-modern-multistep-nav-sep" aria-hidden="true" role="presentation">
2199 <svg width="7" height="11" viewBox="0 0 7 11" fill="none" xmlns="http://www.w3.org/2000/svg" focusable="false" aria-hidden="true">
2200 <path d="M1 1L6 5.5L1 10" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
2201 </svg>
2202 </li>
2203
2204 <li class="wpfnl-modern-multistep-nav-step" role="presentation" data-step="payment">
2205 <button
2206 type="button"
2207 class="wpfnl-modern-multistep-nav-step-btn"
2208 role="tab"
2209 id="wpfnl-step-tab-payment"
2210 aria-controls="wpfnl-step-panel-payment"
2211 aria-selected="false"
2212 data-step="payment"
2213 >
2214 <?php esc_html_e( 'Payment', 'wpfnl' ); ?>
2215 </button>
2216 </li>
2217 </ul>
2218 </nav>
2219 <?php
2220 $fragments['.wpfnl-modern-multistep-nav'] = ob_get_clean();
2221
2222 ob_start();
2223 ?>
2224 <div class="wpfnl-modern-multistep-navigation">
2225 <button
2226 type="button"
2227 class="next-step-btn"
2228 aria-label="Proceed to the next step"
2229 current-step="information"
2230 next-step="<?php echo ( WC()->cart && WC()->cart->needs_shipping_address() ) ? 'billing' : 'payment'; ?>"
2231 >
2232 <?php esc_html_e( 'Proceed to Next Step', 'wpfnl' ); ?>
2233 </button>
2234 </div>
2235 <?php
2236 $fragments['.wpfnl-modern-multistep-navigation'] = ob_get_clean();
2237
2238 // 5. Render #rex-checkout-tabs for multi-step-checkout layout
2239 if ( 'wpfnl-multistep' === $checkout_layout ) {
2240 ob_start();
2241 ?>
2242 <div class="multistep-wrapper" id="rex-checkout-tabs">
2243 <?php if ( WC()->cart && WC()->cart->needs_shipping_address() ) : ?>
2244 <div class="single-step" id="shipping">
2245 <?php
2246 if ( WC()->checkout()->get_checkout_fields() ) {
2247 do_action( 'woocommerce_checkout_before_customer_details' );
2248 do_action( 'woocommerce_checkout_shipping' );
2249 }
2250 ?>
2251 <div class="navigation">
2252 <button type="button" id="shippingNext"><?php echo __( 'Next', 'wpfnl' ); ?></button>
2253 </div>
2254 </div>
2255
2256 <div class="single-step" id="billing">
2257 <?php
2258 if ( WC()->checkout()->get_checkout_fields() ) {
2259 do_action( 'woocommerce_checkout_billing' );
2260 do_action( 'woocommerce_checkout_after_customer_details' );
2261 }
2262 ?>
2263 <div class="navigation">
2264 <button type="button" id="billingPrevious"><?php echo __( 'Previous', 'wpfnl' ); ?></button>
2265 <button type="button" id="billingNext"><?php echo __( 'Next', 'wpfnl' ); ?></button>
2266 </div>
2267 </div>
2268 <?php else : ?>
2269 <div class="single-step" id="billing">
2270 <?php
2271 if ( WC()->checkout()->get_checkout_fields() ) {
2272 do_action( 'woocommerce_checkout_before_customer_details' );
2273 do_action( 'woocommerce_checkout_billing' );
2274 do_action( 'woocommerce_checkout_after_customer_details' );
2275 }
2276 ?>
2277 <div class="navigation">
2278 <button type="button" id="billingNext"><?php echo __( 'Next', 'wpfnl' ); ?></button>
2279 </div>
2280 </div>
2281 <?php endif; ?>
2282
2283 <div class="single-step" id="review">
2284 <?php do_action( 'woocommerce_checkout_before_order_review' ); ?>
2285 <div id="order_review" class="woocommerce-checkout-review-order">
2286 <?php do_action( 'woocommerce_checkout_order_review' ); ?>
2287 </div>
2288 <?php do_action( 'woocommerce_checkout_after_order_review' ); ?>
2289 <div class="navigation">
2290 <button type="button" id="reviewPrevious"><?php echo __( 'Previous', 'wpfnl' ); ?></button>
2291 </div>
2292 </div>
2293 </div>
2294 <?php
2295 $fragments['#rex-checkout-tabs'] = ob_get_clean();
2296 }
2297
2298 return $fragments;
2299 }
2300
2301
2302 /**
2303 * Check that the posted checkout form was rendered by a WPFunnels checkout template.
2304 *
2305 * Every WPFunnels checkout form template prints a `wpfnl_step_id` hidden input inside
2306 * the form, so finding it in the serialized `post_data` is what separates our own
2307 * markup from a theme or page builder checkout that merely runs on a funnel step.
2308 *
2309 * @return bool
2310 * @since 3.14.0
2311 */
2312 private function is_wpfnl_checkout_template_request() {
2313 if ( ! empty( $_POST['wpfnl_step_id'] ) ) {
2314 return true;
2315 }
2316
2317 if ( empty( $_POST['post_data'] ) ) {
2318 return false;
2319 }
2320
2321 $post_data = array();
2322 wp_parse_str( wp_unslash( $_POST['post_data'] ), $post_data );
2323
2324 return ! empty( $post_data['wpfnl_step_id'] );
2325 }
2326
2327
2328 /**
2329 * Display custom coupon field in modern checkout order review section.
2330 *
2331 * @return void
2332 */
2333 public function display_custom_coupon_field() {
2334 if ( ! $this->is_modern_checkout_layout() || ! $this->is_custom_coupon_field_enabled() ) {
2335 return;
2336 }
2337
2338 $is_collapsible = $this->is_collapsible_coupon_enabled();
2339
2340 $coupon_field = apply_filters(
2341 'wpfunnels_coupon_field_options',
2342 array(
2343 'field_text' => __( 'Coupon Code', 'wpfnl' ),
2344 'button_text' => __( 'Apply', 'wpfnl' ),
2345 'class' => '',
2346 )
2347 );
2348
2349 do_action( 'wpfunnels_before_custom_coupon_field_html', $coupon_field );
2350
2351 if ( $is_collapsible ) {
2352 ?>
2353 <div class="wpfnl-coupon-toggle-wrapper">
2354 <div class="wpfnl-coupon-toggle-notice">
2355 <?php echo wp_kses_post( apply_filters( 'wpfunnels_coupon_toggle_text', __( 'Have a coupon?', 'wpfnl' ) ) ); ?>
2356 <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>
2357 </div>
2358 <div class="wpfnl-custom-coupon-field wpfnl-coupon-collapsible <?php echo esc_attr( $coupon_field['class'] ); ?>" id="wpfnl_custom_coupon_field" style="display: none;">
2359 <div class="wpfnl-coupon-col-1">
2360 <span>
2361 <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="">
2362 </span>
2363 </div>
2364 <div class="wpfnl-coupon-col-2">
2365 <span>
2366 <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>
2367 </span>
2368 </div>
2369 </div>
2370 </div>
2371 <?php
2372 } else {
2373 ?>
2374 <div class="wpfnl-custom-coupon-field <?php echo esc_attr( $coupon_field['class'] ); ?>" id="wpfnl_custom_coupon_field">
2375 <div class="wpfnl-coupon-col-1">
2376 <span>
2377 <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="">
2378 </span>
2379 </div>
2380 <div class="wpfnl-coupon-col-2">
2381 <span>
2382 <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>
2383 </span>
2384 </div>
2385 </div>
2386 <?php
2387 }
2388 do_action( 'wpfunnels_after_custom_coupon_field_html', $coupon_field );
2389 }
2390
2391
2392 /**
2393 * Check if custom coupon field is enabled for this checkout.
2394 *
2395 * @return bool
2396 */
2397 private function is_custom_coupon_field_enabled() {
2398 if ( ! apply_filters( 'woocommerce_coupons_enabled', true ) ) {
2399 return false;
2400 }
2401
2402 $checkout_id = 0;
2403 if ( wp_doing_ajax() ) {
2404 $checkout_id = Wpfnl_functions::get_checkout_id_from_post( $_POST );
2405 }
2406
2407 if ( ! $checkout_id ) {
2408 $checkout_id = get_the_ID();
2409 }
2410
2411 if ( ! $checkout_id ) {
2412 return false;
2413 }
2414
2415 $show_coupon = Wpfnl::get_instance()->meta->get_checkout_meta_value( $checkout_id, '_wpfnl_checkout_coupon' );
2416
2417 return 'no' !== $show_coupon;
2418 }
2419
2420
2421 /**
2422 * Check if collapsible coupon field is enabled for this checkout.
2423 *
2424 * @return bool
2425 */
2426 private function is_collapsible_coupon_enabled() {
2427 $checkout_id = 0;
2428 if ( wp_doing_ajax() ) {
2429 $checkout_id = Wpfnl_functions::get_checkout_id_from_post( $_POST );
2430 }
2431
2432 if ( ! $checkout_id ) {
2433 $checkout_id = get_the_ID();
2434 }
2435
2436 if ( ! $checkout_id ) {
2437 return false;
2438 }
2439
2440 $collapsible = get_post_meta( $checkout_id, '_wpfnl_checkout_collapsible_coupon', true );
2441
2442 return 'yes' === $collapsible;
2443 }
2444
2445
2446 /**
2447 * Apply checkout coupon via AJAX.
2448 *
2449 * @return void
2450 */
2451 public function wpfnl_apply_checkout_coupon() {
2452 if ( isset( $_POST['security'] ) && ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['security'] ) ), 'wpfnl' ) ) {
2453 wp_send_json_error( array( 'messages' => '<ul class="woocommerce-error" role="alert"><li>' . esc_html__( 'Invalid request.', 'wpfnl' ) . '</li></ul>' ) );
2454 }
2455
2456 $coupon_code = isset( $_POST['coupon_code'] ) ? wc_format_coupon_code( sanitize_text_field( wp_unslash( $_POST['coupon_code'] ) ) ) : '';
2457 if ( empty( $coupon_code ) ) {
2458 wp_send_json_error( array( 'messages' => '<ul class="woocommerce-error" role="alert"><li>' . esc_html__( 'Please enter a coupon code.', 'wpfnl' ) . '</li></ul>' ) );
2459 }
2460
2461 if ( ! WC()->cart ) {
2462 wp_send_json_error( array( 'messages' => '<ul class="woocommerce-error" role="alert"><li>' . esc_html__( 'Cart is not available.', 'wpfnl' ) . '</li></ul>' ) );
2463 }
2464
2465 $applied = WC()->cart->apply_coupon( $coupon_code );
2466 WC()->cart->calculate_totals();
2467
2468 ob_start();
2469 wc_print_notices();
2470 $messages = ob_get_clean();
2471
2472 if ( $applied ) {
2473 wp_send_json_success( array( 'messages' => $messages ) );
2474 }
2475
2476 wp_send_json_error( array( 'messages' => $messages ) );
2477 }
2478
2479
2480 /**
2481 * Remove checkout coupon via AJAX.
2482 *
2483 * @return void
2484 */
2485 public function wpfnl_remove_checkout_coupon() {
2486 if ( isset( $_POST['security'] ) && ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['security'] ) ), 'wpfnl' ) ) {
2487 wp_send_json_error( array( 'messages' => '<ul class="woocommerce-error" role="alert"><li>' . esc_html__( 'Invalid request.', 'wpfnl' ) . '</li></ul>' ) );
2488 }
2489
2490 $coupon_code = isset( $_POST['coupon_code'] ) ? wc_format_coupon_code( sanitize_text_field( wp_unslash( $_POST['coupon_code'] ) ) ) : '';
2491 if ( empty( $coupon_code ) ) {
2492 wp_send_json_error( array( 'messages' => '<ul class="woocommerce-error" role="alert"><li>' . esc_html__( 'Invalid coupon code.', 'wpfnl' ) . '</li></ul>' ) );
2493 }
2494
2495 if ( ! WC()->cart ) {
2496 wp_send_json_error( array( 'messages' => '<ul class="woocommerce-error" role="alert"><li>' . esc_html__( 'Cart is not available.', 'wpfnl' ) . '</li></ul>' ) );
2497 }
2498
2499 WC()->cart->remove_coupon( $coupon_code );
2500 WC()->cart->calculate_totals();
2501
2502 ob_start();
2503 wc_print_notices();
2504 $messages = ob_get_clean();
2505
2506 wp_send_json_success( array( 'messages' => $messages ) );
2507 }
2508
2509
2510 /**
2511 * Render shipping methods HTML for modern checkout custom section.
2512 *
2513 * @return void
2514 */
2515 public function wpfnl_cart_totals_shipping_html() {
2516 if ( ! Wpfnl_functions::is_wc_active() || ! function_exists( 'WC' ) ) {
2517 return;
2518 }
2519
2520 $packages = WC()->shipping()->get_packages();
2521 if ( empty( $packages ) ) {
2522 WC()->cart->calculate_totals();
2523 $packages = WC()->shipping()->get_packages();
2524 }
2525
2526 $first = true;
2527 foreach ( $packages as $i => $package ) {
2528 $chosen_method = isset( WC()->session->chosen_shipping_methods[ $i ] ) ? WC()->session->chosen_shipping_methods[ $i ] : '';
2529 $product_names = array();
2530
2531 if ( count( $packages ) > 1 ) {
2532 foreach ( $package['contents'] as $item_id => $values ) {
2533 $product_names[ $item_id ] = $values['data']->get_name() . ' &times;' . $values['quantity'];
2534 }
2535 $product_names = apply_filters( 'woocommerce_shipping_package_details_array', $product_names, $package );
2536 }
2537
2538 include WPFNL_DIR . 'public/modules/checkout/templates/checkout/shipping-methods.php';
2539 do_action( 'woocommerce_review_order_after_shipping' );
2540 $first = false;
2541 }
2542 }
2543
2544
2545 /**
2546 * Check if current checkout uses modern templates.
2547 *
2548 * @return bool
2549 */
2550 private function is_modern_checkout_layout() {
2551 $checkout_id = 0;
2552
2553 if ( wp_doing_ajax() ) {
2554 $checkout_id = Wpfnl_functions::get_checkout_id_from_post( $_POST );
2555 }
2556
2557 if ( ! $checkout_id ) {
2558 $checkout_id = get_the_ID();
2559 }
2560
2561 if ( ! $checkout_id ) {
2562 return false;
2563 }
2564
2565 $checkout_layout = Wpfnl_functions::get_checkout_layout( $checkout_id );
2566
2567 return ( false !== strpos( $checkout_layout, 'modern' ) ) || in_array( $checkout_layout, array( 'wpfnl-modern-checkout', 'wpfnl-modern-one-column', 'wpfnl-modern-multistep' ), true );
2568 }
2569
2570
2571 /**
2572 * Set checkout ID hidden field in checkout page.
2573 *
2574 * @param array $checkout
2575 *
2576 * @return void
2577 */
2578 public function wpfnl_set_checkout_id( $checkout ) {
2579 global $post;
2580 $checkout_id = 0;
2581 if ( Wpfnl_functions::is_funnel_checkout_page() ) {
2582 if( isset($post->ID) ){
2583 $checkout_id = $post->ID;
2584 }else{
2585 $checkout_id = isset($_POST['step_id']) ? $_POST['step_id'] : '';
2586 }
2587
2588 }
2589 if( $checkout_id ){
2590 echo '<input type="hidden" class="input-hidden _wpfunnels_checkout_id" name="_wpfunnels_checkout_id" value="' . intval( $checkout_id ) . '">';
2591 }
2592 }
2593
2594
2595 /**
2596 * Redirect funnel checkout after login in checkout page
2597 *
2598 * @param $redirect_url
2599 * @param $user
2600 *
2601 * @return false|string
2602 */
2603 public function wpfnl_redirect_after_login( $redirect_url, $user ) {
2604
2605 if (isset($_POST['_wpfunnels_checkout_id'])) {
2606 $funnel_checkout_id = $_POST['_wpfunnels_checkout_id'];
2607 $redirect_url = get_the_permalink($funnel_checkout_id);
2608 }
2609 return $redirect_url;
2610
2611 }
2612
2613
2614 /**
2615 * Get Custom Woocommerce template
2616 *
2617 * @param $template
2618 * @param $template_name
2619 * @param $template_path
2620 *
2621 * @return mixed|string
2622 */
2623
2624 public static function wpfunnels_woocommerce_locate_template($template, $template_name, $template_path)
2625 {
2626 /***
2627 * Fires when change the wc template
2628 *
2629 * @since 2.8.21
2630 */
2631 if( apply_filters( 'wpfunnels/maybe_locate_template', true ) ){
2632 global $woocommerce;
2633 $_template = $template;
2634 $plugin_path = WPFNL_DIR . '/woocommerce/templates/';
2635
2636 if (file_exists($plugin_path . $template_name)) {
2637 $template = $plugin_path . $template_name;
2638 }
2639
2640 if ( ! $template ) {
2641 $template = $_template;
2642 }
2643 }
2644 return $template;
2645 }
2646
2647
2648 /**
2649 * Discount on checkout
2650 *
2651 * @param $cart_object
2652 */
2653 public function wpfnl_checkout_discount( $cart_object ){
2654
2655 if( !$this->is_funnel_checkout_product_available_on_cart() ){
2656 return;
2657 }
2658
2659 if ( is_admin() && wp_doing_ajax() ){
2660 return;
2661 }
2662 if( isset( $_POST['wpfnl_offer_page'] ) ){
2663 return;
2664 }
2665
2666 $step_id = Wpfnl_functions::get_checkout_id_from_post( $_POST );
2667
2668 if( !$step_id ){
2669 $step_id = isset($_POST['step_id']) ? $_POST['step_id'] : '';
2670 }
2671
2672 if( !$step_id ){
2673 $step_id = isset($_POST['_wpfunnels_checkout_id']) ? $_POST['_wpfunnels_checkout_id'] : '';
2674 }
2675
2676 if( !$step_id ) {
2677 return;
2678 }
2679
2680 $funnel_id = Wpfnl_functions::get_funnel_id_from_step( $step_id );
2681
2682 if( !$funnel_id ){
2683 return;
2684 }
2685
2686 $discount_instance = new WpfnlDiscount();
2687
2688 if( $discount_instance->maybe_time_bound_discount( $step_id ) && !$discount_instance->maybe_validate_discount_time( $step_id )) {
2689 return;
2690 }
2691
2692 $discount = $discount_instance->get_discount_settings( $step_id );
2693
2694 if( !is_array($discount) ){
2695 return;
2696 }
2697
2698 if( (isset($discount['discountOptions']) && $discount['discountOptions'] !== 'original') ){
2699 if( 'regular' === $discount['discountapplyto'] ){
2700 foreach (WC()->cart->get_cart() as $cart_item_key => $cart_item) {
2701 $product = $cart_item['data'];
2702 if (isset($cart_item['bundled_by']) || $product->is_type('bundle') ) { // 'bundled_by' is a common meta key used for bundled items
2703 $regular_price = $product->get_regular_price();
2704 $regular_price = $regular_price ? $regular_price : $product->get_price();
2705 $cart_item['data']->set_price($regular_price);
2706 }
2707 }
2708 }
2709
2710 $helper = CheckoutHelper::getInstance();
2711 $total = $helper->get_custom_subtotal();
2712 $discount_amount = $discount_instance->get_discount_amount( $discount['discountOptions'], $discount['mutedDiscountValue'], $total );
2713 $discount_amount = filter_var($discount_amount, FILTER_SANITIZE_NUMBER_FLOAT, FILTER_FLAG_ALLOW_FRACTION);
2714 $discount_amount = apply_filters('wpfunnels/checkout_discount_amount', $discount_amount);
2715 $discount_amount = floatval($discount_amount);
2716 $cart_object->add_fee( __('Discount','wpfnl'), -$discount_amount, true,'standard');
2717 }
2718 }
2719
2720
2721 /**
2722 * Check whether funnel checkout product is available on cart or not
2723 *
2724 * @return boolean true if funnel checkout product is available on cart, false otherwise
2725 *
2726 * @access public
2727 * @since 3.4.15
2728 */
2729 public function is_funnel_checkout_product_available_on_cart() {
2730 if( WC()->cart->is_empty() ){
2731 return false;
2732 }
2733 $is_available = false;
2734 foreach (WC()->cart->get_cart() as $cart_item_key => $cart_item) {
2735 if( !isset( $cart_item['wpfnl_order_bump']) ){
2736 $is_available = true;
2737 break;
2738 }
2739 }
2740 return $is_available;
2741 }
2742
2743
2744 /**
2745 * Get price type
2746 */
2747 public function get_product_price_type( $funnel_id, $step_id ){
2748
2749 $is_gbf = get_post_meta( $funnel_id, 'is_global_funnel', true );
2750 if( $is_gbf == 'no' || !$is_gbf ){
2751 $discount_instance = new WpfnlDiscount();
2752 if( $discount_instance->maybe_time_bound_discount( $step_id ) && !$discount_instance->maybe_validate_discount_time( $step_id ) ) {
2753 return 'original';
2754 }
2755
2756 $discount = $discount_instance->get_discount_settings( $step_id );
2757 if( is_array($discount) ){
2758 if( $discount['discountOptions'] !== 'original' ){
2759 if( $discount['discountapplyto'] == 'regular' ){
2760 return 'regular';
2761 }else{
2762 return 'sale';
2763 }
2764 }
2765 }
2766 }
2767 return 'original';
2768 }
2769
2770 /**
2771 * After optin form submition set cookie data
2772 * Use cookes data for checkout billings fields
2773 *
2774 * @param $step_id
2775 * @param $post_action
2776 * @param $action_type
2777 * @param $record
2778 */
2779
2780 public function get_optin_data_checkout($step_id, $post_action, $action_type, $record){
2781 if(isset($record->form_data['data_to_checkout']) && 'yes' == $record->form_data['data_to_checkout']){
2782 /**
2783 * Set Cookie Data
2784 */
2785 $cookie_name = 'wpfunnels_send_data_checkout';
2786 $cookie = isset( $_COOKIE[$cookie_name] ) ? json_decode( wp_unslash( $_COOKIE[$cookie_name] ), true ) : array();
2787 $cookie['after_optin_submit_send_for_checkout'] = ( isset($record->form_data['email']) && $record->form_data['email'] ) ? $record->form_data : false;
2788 ob_start();
2789 setcookie( $cookie_name, wp_json_encode( $cookie ), time() + 3600 * 6, '/', COOKIE_DOMAIN );
2790 ob_end_flush();
2791 }
2792 }
2793
2794 /**
2795 * Get Optin cookes data in checkout fields
2796 *
2797 * @param $fields
2798 *
2799 * @return array
2800 */
2801 public function set_option_data_in_checkout_filed($fields)
2802 {
2803 $cookie_name = 'wpfunnels_send_data_checkout';
2804 $cookie = isset( $_COOKIE[$cookie_name] ) ? json_decode( wp_unslash( $_COOKIE[$cookie_name] ), true ) : array();
2805 if(!empty($cookie)){
2806 if(isset($cookie['after_optin_submit_send_for_checkout'])){
2807 $first_name = isset($cookie['after_optin_submit_send_for_checkout']['first_name']) ? $cookie['after_optin_submit_send_for_checkout']['first_name'] : '';
2808 $last_name = isset($cookie['after_optin_submit_send_for_checkout']['last_name']) ? $cookie['after_optin_submit_send_for_checkout']['last_name'] : '';
2809 $phone = isset($cookie['after_optin_submit_send_for_checkout']['phone']) ? $cookie['after_optin_submit_send_for_checkout']['phone'] : '';
2810 $email = isset($cookie['after_optin_submit_send_for_checkout']['email']) ? $cookie['after_optin_submit_send_for_checkout']['email'] : '';
2811 $fields['billing']['billing_first_name']['default'] = $first_name;
2812 $fields['billing']['billing_last_name']['default'] = $last_name;
2813 $fields['billing']['billing_phone']['default'] = $phone;
2814 $fields['billing']['billing_email']['default'] = $email;
2815 }
2816 }
2817
2818 $checkout_id = 0;
2819
2820 if ( wp_doing_ajax() ) {
2821 if ( isset( $_POST['step_id'] ) ) {
2822 $checkout_id = intval( wp_unslash( $_POST['step_id'] ) );
2823 } elseif ( isset( $_POST['_wpfunnels_checkout_id'] ) ) {
2824 $checkout_id = intval( wp_unslash( $_POST['_wpfunnels_checkout_id'] ) );
2825 } elseif ( isset( $_POST['post_data'] ) ) {
2826 $post_data = array();
2827 parse_str( wp_unslash( $_POST['post_data'] ), $post_data );
2828 if ( isset( $post_data['_wpfunnels_checkout_id'] ) ) {
2829 $checkout_id = intval( $post_data['_wpfunnels_checkout_id'] );
2830 }
2831 }
2832 }
2833
2834 if ( ! $checkout_id ) {
2835 $checkout_id = get_the_ID();
2836 }
2837
2838 if ( $checkout_id ) {
2839 if (
2840 Wpfnl_functions::check_if_this_is_step_type_by_id( $checkout_id, 'checkout' ) &&
2841 isset( $fields['order']['order_comments']['placeholder'] )
2842 ) {
2843 $fields['order']['order_comments']['placeholder'] = esc_attr__( 'Notes about your order', 'wpfnl' );
2844 }
2845
2846 $checkout_layout = Wpfnl_functions::get_checkout_layout( $checkout_id );
2847
2848 if ( 'wpfnl-modern-checkout' === $checkout_layout ) {
2849 if ( isset( $fields['account']['account_username'] ) ) {
2850 unset( $fields['account']['account_username'] );
2851 }
2852 if ( isset( $fields['account']['account_password'] ) ) {
2853 unset( $fields['account']['account_password'] );
2854 }
2855 }
2856 }
2857
2858 return $fields;
2859 }
2860
2861
2862
2863 /**
2864 * Modifies the available payment gateways based on certain conditions.
2865 *
2866 * @param array $available_gateways The array of available payment gateways.
2867 * @return array The modified array of available payment gateways.
2868 *
2869 * @since 3.1.8
2870 */
2871 public function conditional_payment_gateways( $available_gateways ){
2872 if( defined('WPFNL_PRO_VERSION') ){
2873 $is_checkout = Wpfnl_functions::is_funnel_checkout_page();
2874 if( $is_checkout['status'] ){
2875 $is_funnel_checkout = Wpfnl_functions::check_if_this_is_step_type_by_id( $is_checkout['id'], 'checkout' );
2876 if( $is_funnel_checkout ){
2877 $disableGateways = get_post_meta($is_checkout['id'], '_wpfnl_disabled_payemnts', true);
2878 if( is_array($disableGateways) && !empty($disableGateways ) ){
2879 foreach( $available_gateways as $key=>$gateway ){
2880 if (in_array($key, $disableGateways)){
2881 unset($available_gateways[$key]);
2882 }
2883 }
2884 }
2885
2886 }
2887 }
2888 }
2889 return $available_gateways;
2890 }
2891 }
2892