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
← All changes | public/modules/checkout/class-wpfnl-checkout.php +655 -56 3.12.13 → 3.13.3 View file →
@@ -8,9 +8,8 @@
8 8
9 9 namespace WPFunnels\Modules\Frontend\Checkout;
10 10
11 11 use Error;
12 -use WC_Session_Handler;
13 12 use WPFunnels\Data_Store\Wpfnl_Steps_Store_Data;
14 13 use WPFunnels\Frontend\Module\Wpfnl_Frontend_Module;
15 14 use WPFunnels\Frontend\Wpfnl_Public;
16 15 use WPFunnels\Meta\Wpfnl_Default_Meta;
@@ -33,8 +32,22 @@
33 32 public $funnel_id;
34 33
35 34 public $step_id;
36 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 +
37 50 // const CHECKOUT = "";
38 51
39 52 public function __construct()
40 53 {
@@ -120,9 +133,11 @@
120 133 add_action( 'wpfunnels/after_optin_submit', array( $this, 'get_optin_data_checkout' ), 10, 4 );
121 134 add_filter( 'woocommerce_checkout_fields', array($this, 'set_option_data_in_checkout_filed'),10 );
122 135 add_filter( 'woocommerce_available_payment_gateways', array($this, 'conditional_payment_gateways'),10 );
123 136 add_action( 'woocommerce_checkout_after_customer_details', array( $this, 'add_custom_shipping_section' ), 10 );
124 - add_action( 'woocommerce_checkout_order_review', array( $this, 'display_custom_coupon_field' ) );
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 );
125 140
126 141 /* Manage Express Checkout buttons visibility dynamically */
127 142 add_filter( 'wc_stripe_show_payment_request_on_checkout', [$this, 'wpfnl_manage_stripe_express_checkout'], 9999, 2 );
128 143
@@ -155,9 +170,9 @@
155 170
156 171 if ( $checkout_id ) {
157 172 $express_checkout_enabled = get_post_meta( $checkout_id, '_wpfnl_express_checkout_enabled', true );
158 173 if ( 'yes' !== $express_checkout_enabled ) {
159 - return false; // Hide Stripe Payment Request (Apple Pay / Google Pay)
174 + return false; // Hide Stripe Payment Request (Apple Pay / Google Pay)
160 175 }
161 176 }
162 177
163 178 return $show;
@@ -429,8 +444,11 @@
429 444 * Init necessary actions related Checkout step
430 445 */
431 446 public function init_actions() {
432 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 );
433 451 $this->display_coupon_field();
434 452 $this->enable_checkout_hook();
435 453
436 454 if ( Wpfnl_functions::check_if_this_is_step_type( 'checkout' ) ) {
@@ -445,12 +463,45 @@
445 463 * @since 3.1.3
446 464 */
447 465 public function load_checkout_step_scripts() {
448 466 $this->load_google_map_api_scripts();
467 + $this->load_tokenization_form_script();
449 468 }
450 469
451 470
452 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 + /**
453 504 * Load google map places script on checkout page
454 505 *
455 506 * @since 3.1.3
456 507 */
@@ -580,10 +631,9 @@
580 631 $user_id = $order->get_user_id();
581 632 $orders[ $checkout_id ] = $order_id;
582 633 WC()->session->set( 'wpfnl_orders_' . $user_id . '_' . $funnel_id, $orders );
583 634
584 - $session_handler = new WC_Session_Handler();
585 - WC()->session->set( 'wpfnl_order_owner', $session_handler->generate_customer_id() );
635 + Wpfnl_functions::mark_order_owned_by_session( $order_id );
586 636
587 637 /**
588 638 * Fires after a funnel order is placed.
589 639 *
@@ -621,9 +671,9 @@
621 671
622 672 if(!Wpfnl_functions::is_wc_active() ){
623 673 return;
624 674 }
625 -
675 +
626 676 $checkout_id = '';
627 677 if( wp_doing_ajax() ){
628 678 $checkout_id = Wpfnl_functions::get_checkout_id_from_post($_POST);
629 679 }
@@ -638,8 +688,9 @@
638 688 || Wpfnl_functions::is_checkoutify_orderbump_clicked_from_post_data()
639 689 || Wpfnl_functions::is_product_option_from_post_data()
640 690 || Wpfnl_functions::is_variation_selected_from_post_data()
641 691 || Wpfnl_functions::maybe_select_quantity_from_post_data()
692 + || Wpfnl_functions::is_order_bump_already_accepted_from_cookie()
642 693 // || Wpfnl_functions::is_coupon_applied_from_post_data()
643 694 ) {
644 695 return;
645 696 }
@@ -1093,11 +1144,11 @@
1093 1144 }
1094 1145 if(!$checkout_id) {
1095 1146 return '';
1096 1147 }
1097 - $checkout_layout = Wpfnl::get_instance()->meta->get_checkout_meta_value($checkout_id, 'wpfnl_checkout_layout', 'wpfnl-col-2');
1148 + $checkout_layout = Wpfnl_functions::get_checkout_layout($checkout_id);
1098 1149
1099 - $floating_label = Wpfnl::get_instance()->meta->get_checkout_meta_value($checkout_id, 'wpfnl_floating_label', '');
1150 + $floating_label = Wpfnl_functions::get_checkout_step_setting($checkout_id, 'wpfnl_floating_label', '');
1100 1151
1101 1152 $output = '';
1102 1153 ob_start();
1103 1154
@@ -1194,10 +1245,15 @@
1194 1245 * @since 1.0.0
1195 1246 */
1196 1247 public function checkout_shortcode_metas()
1197 1248 {
1249 + if ($this->checkout_metas_rendered) {
1250 + return;
1251 + }
1252 +
1198 1253 if (Wpfnl_functions::check_if_this_is_step_type('checkout')) {
1199 1254 global $post;
1255 + $this->checkout_metas_rendered = true;
1200 1256 $this->step_id = $post->ID;
1201 1257 $step = new Wpfnl_Steps_Store_Data();
1202 1258 $step->read($this->step_id);
1203 1259 $this->funnel_id = $step->get_funnel_id();
@@ -1209,8 +1265,94 @@
1209 1265 }
1210 1266
1211 1267
1212 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 + /**
1213 1355 * Insert helper hidden ids
1214 1356 *
1215 1357 * @since 1.0.0
1216 1358 */
@@ -1336,9 +1478,9 @@
1336 1478 * Returns the query parameter after applying the 'wpfunnels/prepare_query_param' filter.
1337 1479 *
1338 1480 * @param array $query_param
1339 1481 * @return array $query_param
1340 - *
1482 + *
1341 1483 * @since 3.4.13
1342 1484 */
1343 1485 return apply_filters( 'wpfunnels/prepare_query_param', $query_param );
1344 1486 }
@@ -1447,8 +1589,12 @@
1447 1589 *
1448 1590 * @since 1.1.
1449 1591 */
1450 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 +
1451 1597 $step_id = filter_input( INPUT_POST, 'step_id', FILTER_VALIDATE_INT );
1452 1598 $product_id = filter_input( INPUT_POST, 'product', FILTER_VALIDATE_INT );
1453 1599 $variation_id = filter_input( INPUT_POST, 'variation_id', FILTER_VALIDATE_INT );
1454 1600 $checker = filter_input( INPUT_POST, 'checker', FILTER_SANITIZE_SPECIAL_CHARS );
@@ -1459,17 +1605,33 @@
1459 1605 $funnel_id = Wpfnl_functions::get_funnel_id_from_step( $step_id );
1460 1606 $type = get_post_meta( $funnel_id, '_wpfnl_funnel_type', true );
1461 1607 $type = !$type ? 'wc' : $type;
1462 1608
1463 - // Validate quantity - must be a positive integer
1464 - $quantity = absint( $quantity );
1465 - if ( $quantity < 1 ) {
1466 - $quantity = 1;
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;
1467 1632 }
1468 1633
1469 - // If variation_id exists, use it as product_id for cart functions.
1470 - $cart_product_id = $variation_id ? $variation_id : $product_id;
1471 -
1472 1634 $class_object = Wpfnl_Public_Type_Factory::build( $type );
1473 1635 if( $class_object ) {
1474 1636 $data = $class_object->wpfnl_order_bump_trigger( $step_id, $cart_product_id, $quantity, $key, $user_id, $funnel_id, $checker );
1475 1637 if( $type === 'lms' ) {
@@ -1694,8 +1856,11 @@
1694 1856 public function add_custom_shipping_section() {
1695 1857 if ( ! $this->is_modern_checkout_layout() || ! apply_filters( 'wpfunnels_should_render_custom_shipping', true ) ) {
1696 1858 return;
1697 1859 }
1860 + if ( ! Wpfnl_functions::is_wc_active() || ! function_exists( 'WC' ) || ! WC()->cart || ! WC()->cart->needs_shipping() ) {
1861 + return;
1862 + }
1698 1863 ob_start();
1699 1864 echo '<div class="wpfnl-customer-shipping">';
1700 1865 $this->wpfnl_cart_totals_shipping_html();
1701 1866 echo '</div>';
@@ -1703,8 +1868,465 @@
1703 1868 }
1704 1869
1705 1870
1706 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 + /**
1707 2329 * Display custom coupon field in modern checkout order review section.
1708 2330 *
1709 2331 * @return void
1710 2332 */
@@ -1778,19 +2400,9 @@
1778 2400 }
1779 2401
1780 2402 $checkout_id = 0;
1781 2403 if ( wp_doing_ajax() ) {
1782 - if ( isset( $_POST['step_id'] ) ) {
1783 - $checkout_id = intval( wp_unslash( $_POST['step_id'] ) );
1784 - } elseif ( isset( $_POST['_wpfunnels_checkout_id'] ) ) {
1785 - $checkout_id = intval( wp_unslash( $_POST['_wpfunnels_checkout_id'] ) );
1786 - } elseif ( isset( $_POST['post_data'] ) ) {
1787 - $post_data = array();
1788 - parse_str( wp_unslash( $_POST['post_data'] ), $post_data );
1789 - if ( isset( $post_data['_wpfunnels_checkout_id'] ) ) {
1790 - $checkout_id = intval( $post_data['_wpfunnels_checkout_id'] );
1791 - }
1792 - }
2404 + $checkout_id = Wpfnl_functions::get_checkout_id_from_post( $_POST );
1793 2405 }
1794 2406
1795 2407 if ( ! $checkout_id ) {
1796 2408 $checkout_id = get_the_ID();
@@ -1801,9 +2413,9 @@
1801 2413 }
1802 2414
1803 2415 $show_coupon = Wpfnl::get_instance()->meta->get_checkout_meta_value( $checkout_id, '_wpfnl_checkout_coupon' );
1804 2416
1805 - return 'yes' === $show_coupon;
2417 + return 'no' !== $show_coupon;
1806 2418 }
1807 2419
1808 2420
1809 2421 /**
@@ -1813,19 +2425,9 @@
1813 2425 */
1814 2426 private function is_collapsible_coupon_enabled() {
1815 2427 $checkout_id = 0;
1816 2428 if ( wp_doing_ajax() ) {
1817 - if ( isset( $_POST['step_id'] ) ) {
1818 - $checkout_id = intval( wp_unslash( $_POST['step_id'] ) );
1819 - } elseif ( isset( $_POST['_wpfunnels_checkout_id'] ) ) {
1820 - $checkout_id = intval( wp_unslash( $_POST['_wpfunnels_checkout_id'] ) );
1821 - } elseif ( isset( $_POST['post_data'] ) ) {
1822 - $post_data = array();
1823 - parse_str( wp_unslash( $_POST['post_data'] ), $post_data );
1824 - if ( isset( $post_data['_wpfunnels_checkout_id'] ) ) {
1825 - $checkout_id = intval( $post_data['_wpfunnels_checkout_id'] );
1826 - }
1827 - }
2429 + $checkout_id = Wpfnl_functions::get_checkout_id_from_post( $_POST );
1828 2430 }
1829 2431
1830 2432 if ( ! $checkout_id ) {
1831 2433 $checkout_id = get_the_ID();
@@ -1948,19 +2550,9 @@
1948 2550 private function is_modern_checkout_layout() {
1949 2551 $checkout_id = 0;
1950 2552
1951 2553 if ( wp_doing_ajax() ) {
1952 - if ( isset( $_POST['step_id'] ) ) {
1953 - $checkout_id = intval( wp_unslash( $_POST['step_id'] ) );
1954 - } elseif ( isset( $_POST['_wpfunnels_checkout_id'] ) ) {
1955 - $checkout_id = intval( wp_unslash( $_POST['_wpfunnels_checkout_id'] ) );
1956 - } elseif ( isset( $_POST['post_data'] ) ) {
1957 - $post_data = array();
1958 - parse_str( wp_unslash( $_POST['post_data'] ), $post_data );
1959 - if ( isset( $post_data['_wpfunnels_checkout_id'] ) ) {
1960 - $checkout_id = intval( $post_data['_wpfunnels_checkout_id'] );
1961 - }
1962 - }
2554 + $checkout_id = Wpfnl_functions::get_checkout_id_from_post( $_POST );
1963 2555 }
1964 2556
1965 2557 if ( ! $checkout_id ) {
1966 2558 $checkout_id = get_the_ID();
@@ -1969,11 +2561,11 @@
1969 2561 if ( ! $checkout_id ) {
1970 2562 return false;
1971 2563 }
1972 2564
1973 - $checkout_layout = Wpfnl::get_instance()->meta->get_checkout_meta_value( $checkout_id, 'wpfnl_checkout_layout', 'wpfnl-col-2' );
2565 + $checkout_layout = Wpfnl_functions::get_checkout_layout( $checkout_id );
1974 2566
1975 - return in_array( $checkout_layout, array( 'wpfnl-modern-checkout', 'wpfnl-modern-one-column' ), true );
2567 + return ( false !== strpos( $checkout_layout, 'modern' ) ) || in_array( $checkout_layout, array( 'wpfnl-modern-checkout', 'wpfnl-modern-one-column', 'wpfnl-modern-multistep' ), true );
1976 2568 }
1977 2569
1978 2570
1979 2571 /**
@@ -2101,9 +2693,9 @@
2101 2693
2102 2694 if( !is_array($discount) ){
2103 2695 return;
2104 2696 }
2105 -
2697 +
2106 2698 if( (isset($discount['discountOptions']) && $discount['discountOptions'] !== 'original') ){
2107 2699 if( 'regular' === $discount['discountapplyto'] ){
2108 2700 foreach (WC()->cart->get_cart() as $cart_item_key => $cart_item) {
2109 2701 $product = $cart_item['data'];
@@ -2127,11 +2719,11 @@
2127 2719
2128 2720
2129 2721 /**
2130 2722 * Check whether funnel checkout product is available on cart or not
2131 - *
2723 + *
2132 2724 * @return boolean true if funnel checkout product is available on cart, false otherwise
2133 - *
2725 + *
2134 2726 * @access public
2135 2727 * @since 3.4.15
2136 2728 */
2137 2729 public function is_funnel_checkout_product_available_on_cart() {
@@ -2243,9 +2835,16 @@
2243 2835 $checkout_id = get_the_ID();
2244 2836 }
2245 2837
2246 2838 if ( $checkout_id ) {
2247 - $checkout_layout = Wpfnl::get_instance()->meta->get_checkout_meta_value( $checkout_id, 'wpfnl_checkout_layout', 'wpfnl-col-2' );
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 );
2248 2847
2249 2848 if ( 'wpfnl-modern-checkout' === $checkout_layout ) {
2250 2849 if ( isset( $fields['account']['account_username'] ) ) {
2251 2850 unset( $fields['account']['account_username'] );