Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(modal-checkout): optimize iframe load and refactor anonymous cart generation #1896

Merged
merged 22 commits into from
Oct 11, 2024
Merged
Show file tree
Hide file tree
Changes from 21 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
e48e12c
fix(modal-checkout): optimize iframe load
miguelpeixe Oct 8, 2024
f3f6992
fix: prevent multiple runners
miguelpeixe Oct 8, 2024
f0972b7
fix: allow `newspack-newsletters-` scripts
miguelpeixe Oct 8, 2024
abb28ac
fix: call cb on early return and log missing jquery
miguelpeixe Oct 8, 2024
07f5df3
chore: log when unable to block form
miguelpeixe Oct 8, 2024
f55c4bb
fix: fix iframe ready check and remove ie8 support
miguelpeixe Oct 8, 2024
25f6118
fix: remove `init_checkout` listener
miguelpeixe Oct 8, 2024
7dcfc12
fix: immediately invoked function
miguelpeixe Oct 9, 2024
7929abe
Merge branch 'epic/ras-acc' into fix/optimize-modal-checkout-load
miguelpeixe Oct 9, 2024
7b3ccd9
Merge branch 'epic/ras-acc' into fix/optimize-modal-checkout-load
miguelpeixe Oct 10, 2024
27d3bc4
feat: add to cart async with the auth flow
miguelpeixe Oct 11, 2024
12b8f26
chore: lint
miguelpeixe Oct 11, 2024
3cfbf80
fix: refactor anonymous cart handling
miguelpeixe Oct 11, 2024
047850d
fix: update method names
miguelpeixe Oct 11, 2024
893a9c4
feat: filter allowed assets
miguelpeixe Oct 11, 2024
35b6d59
fix: restore `is_newspack_donate` and use guard clause
miguelpeixe Oct 11, 2024
6a68ca6
fix: variable position
miguelpeixe Oct 11, 2024
50f0e68
fix: isolate checkout registration flag
miguelpeixe Oct 11, 2024
68c787c
chore: remove extra line
miguelpeixe Oct 11, 2024
af02c11
fix: use url query param for checkout registration flag
miguelpeixe Oct 11, 2024
bffe929
chore: improve docblock
miguelpeixe Oct 11, 2024
21f542c
fix: restore checkout url params functionality
miguelpeixe Oct 11, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
286 changes: 177 additions & 109 deletions includes/class-modal-checkout.php
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,12 @@ final class Modal_Checkout {
* Initialize hooks.
*/
public static function init() {
add_action( 'wp_loaded', [ __CLASS__, 'process_checkout_request' ], 5 );
// Process checkout request.
add_action( 'wp_ajax_modal_checkout_request', [ __CLASS__, 'process_checkout_request' ] );
add_action( 'wp_ajax_nopriv_modal_checkout_request', [ __CLASS__, 'process_checkout_request' ] );
add_action( 'wp', [ __CLASS__, 'process_checkout_request' ] );

add_action( 'wp_loaded', [ __CLASS__, 'set_checkout_registration_flag' ] );
add_filter( 'wp_redirect', [ __CLASS__, 'pass_url_param_on_redirect' ] );
add_filter( 'woocommerce_cart_product_cannot_be_purchased_message', [ __CLASS__, 'woocommerce_cart_product_cannot_be_purchased_message' ], 10, 2 );
add_filter( 'woocommerce_add_error', [ __CLASS__, 'hide_expiry_message_shop_link' ] );
Expand Down Expand Up @@ -88,7 +93,7 @@ public static function init() {
add_action( 'woocommerce_checkout_create_order', [ __CLASS__, 'maybe_add_checkout_registration_order_meta' ], 10, 1 );

// Remove some stuff from the modal checkout page. It's displayed in an iframe, so it should not be treated as a separate page.
add_action( 'wp_enqueue_scripts', [ __CLASS__, 'dequeue_scripts' ], 11 );
add_action( 'wp_enqueue_scripts', [ __CLASS__, 'dequeue_scripts' ], PHP_INT_MAX );
add_filter( 'newspack_reader_activation_should_render_auth', [ __CLASS__, 'is_not_modal_checkout_filter' ] );
add_filter( 'newspack_enqueue_reader_activation_block', [ __CLASS__, 'is_not_modal_checkout_filter' ] );
add_filter( 'newspack_enqueue_memberships_block_patterns', [ __CLASS__, 'is_not_modal_checkout_filter' ] );
Expand Down Expand Up @@ -147,148 +152,158 @@ public static function dequeue_woocommerce_styles( $enqueue_styles ) {
return $enqueue_styles;
}

/**
* Set the checkout registration flag to WC session.
*/
public static function set_checkout_registration_flag() {
// Flag the checkout as a registration.
$is_checkout_registration = filter_input( INPUT_GET, self::CHECKOUT_REGISTRATION_FLAG, FILTER_SANITIZE_NUMBER_INT );
if ( $is_checkout_registration ) {
\WC()->session->set( self::CHECKOUT_REGISTRATION_FLAG, true );
}
}

/**
* Process checkout request for modal.
*/
public static function process_checkout_request() {
if ( is_admin() ) {
if ( is_admin() && ! defined( 'DOING_AJAX' ) ) {
return;
}

$is_newspack_checkout = filter_input( INPUT_GET, 'newspack_checkout', FILTER_SANITIZE_NUMBER_INT );

if ( ! $is_newspack_checkout ) {
return;
}

$is_newspack_checkout = filter_input( INPUT_GET, 'newspack_checkout', FILTER_SANITIZE_NUMBER_INT );
$is_newspack_donate = filter_input( INPUT_GET, 'newspack_donate', FILTER_SANITIZE_NUMBER_INT );
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I removed this because donations are not handled here. Please let me know if I'm missing something.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IIRC, this method will set the registration checkout flag for donations, which is why this was added. This was to set some order meta so we know whether to send a regular thank you email, or a welcome + thank you email. Maybe there is a better way to handle this now with the async cart.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it working? Because donation checkout requests are processed in a different method. We can replicate the flag logic there.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah I see it works because it's before the is_newspack_product check. WDYT we replicate it to the donate request method?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm... Given the entire logic for the checkout registration flag lives in the blocks plugin, I see why it's better to have it there. I'll restore it.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated in 35b6d59. I've also changed the is_newspack_checkout to be a guard clause for consistency.

Copy link
Member Author

@miguelpeixe miguelpeixe Oct 11, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just now realized that because of the new strategy, the registration flag has to be handled differently. The anonymous cart generation doesn't know if it's a registration or not. Moved in 50f0e68 so this can set the WC session var.

$product_id = filter_input( INPUT_GET, 'product_id', FILTER_SANITIZE_NUMBER_INT );
$variation_id = filter_input( INPUT_GET, 'variation_id', FILTER_SANITIZE_NUMBER_INT );
$after_success_behavior = filter_input( INPUT_GET, 'after_success_behavior', FILTER_SANITIZE_SPECIAL_CHARS );
$after_success_url = filter_input( INPUT_GET, 'after_success_url', FILTER_SANITIZE_URL );
$after_success_button_label = filter_input( INPUT_GET, 'after_success_button_label', FILTER_SANITIZE_SPECIAL_CHARS );
$is_checkout_registration = filter_input( INPUT_GET, self::CHECKOUT_REGISTRATION_FLAG, FILTER_SANITIZE_NUMBER_INT );

if ( ! $is_newspack_checkout && ! $is_newspack_donate ) {
if ( ! $product_id ) {
return;
}

// Flag the checkout as a registration.
if ( $is_checkout_registration ) {
\WC()->session->set( self::CHECKOUT_REGISTRATION_FLAG, true );
if ( $variation_id ) {
$product_id = $variation_id;
}

if ( $is_newspack_checkout ) {
if ( ! $product_id ) {
return;
}

if ( $variation_id ) {
$product_id = $variation_id;
}
$referer = wp_get_referer();
$params = [];
$parsed_url = wp_parse_url( $referer );

$referer = wp_get_referer();
$params = [];
$parsed_url = wp_parse_url( $referer );
// Get URL params appended to the referer URL.
if ( ! empty( $parsed_url['query'] ) ) {
wp_parse_str( $parsed_url['query'], $params );
}

// Get URL params appended to the referer URL.
if ( ! empty( $parsed_url['query'] ) ) {
wp_parse_str( $parsed_url['query'], $params );
}
$params = array_merge( $params, compact( 'after_success_behavior', 'after_success_url', 'after_success_button_label' ) );

$params = array_merge( $params, compact( 'after_success_behavior', 'after_success_url', 'after_success_button_label' ) );
if ( function_exists( 'wpcom_vip_url_to_postid' ) ) {
$referer_post_id = wpcom_vip_url_to_postid( $referer );
} else {
$referer_post_id = url_to_postid( $referer ); // phpcs:ignore WordPressVIPMinimum.Functions.RestrictedFunctions.url_to_postid_url_to_postid
}

if ( function_exists( 'wpcom_vip_url_to_postid' ) ) {
$referer_post_id = wpcom_vip_url_to_postid( $referer );
} else {
$referer_post_id = url_to_postid( $referer ); // phpcs:ignore WordPressVIPMinimum.Functions.RestrictedFunctions.url_to_postid_url_to_postid
}
$referer_tags = [];
$referer_categories = [];
$tags = get_the_tags( $referer_post_id );
if ( $tags && ! empty( $tags ) ) {
$referer_tags = array_map(
function ( $item ) {
return $item->slug;
},
$tags
);
}

$referer_tags = [];
$referer_categories = [];
$tags = get_the_tags( $referer_post_id );
if ( $tags && ! empty( $tags ) ) {
$referer_tags = array_map(
function ( $item ) {
return $item->slug;
},
$tags
);
}
$categories = get_the_category( $referer_post_id );
if ( $categories && ! empty( $categories ) ) {
$referer_categories = array_map(
function ( $item ) {
return $item->slug;
},
$categories
);
}

$categories = get_the_category( $referer_post_id );
if ( $categories && ! empty( $categories ) ) {
$referer_categories = array_map(
function ( $item ) {
return $item->slug;
},
$categories
);
}
$cart_item_data = self::amend_cart_item_data( [ 'referer' => $referer ] );

$cart_item_data = self::amend_cart_item_data( [ 'referer' => $referer ] );
$newspack_popup_id = filter_input( INPUT_GET, 'newspack_popup_id', FILTER_SANITIZE_NUMBER_INT );
if ( $newspack_popup_id ) {
$cart_item_data['newspack_popup_id'] = $newspack_popup_id;
}

$newspack_popup_id = filter_input( INPUT_GET, 'newspack_popup_id', FILTER_SANITIZE_NUMBER_INT );
if ( $newspack_popup_id ) {
$cart_item_data['newspack_popup_id'] = $newspack_popup_id;
/** Apply NYP custom price */
$price = filter_input( INPUT_GET, 'price', FILTER_SANITIZE_FULL_SPECIAL_CHARS );
if ( \Newspack_Blocks::can_use_name_your_price() ? \WC_Name_Your_Price_Helpers::is_nyp( $product_id ) : false ) {
if ( empty( $price ) ) {
$price = \WC_Name_Your_Price_Helpers::get_suggested_price( $product_id );
}
$min_price = \WC_Name_Your_Price_Helpers::get_minimum_price( $product_id );
$max_price = \WC_Name_Your_Price_Helpers::get_maximum_price( $product_id );
$price = ! empty( $max_price ) ? min( $price, $max_price ) : $price;
$price = ! empty( $min_price ) ? max( $price, $min_price ) : $price;

/** Apply NYP custom price */
$price = filter_input( INPUT_GET, 'price', FILTER_SANITIZE_FULL_SPECIAL_CHARS );
if ( \Newspack_Blocks::can_use_name_your_price() ? \WC_Name_Your_Price_Helpers::is_nyp( $product_id ) : false ) {
if ( empty( $price ) ) {
$price = \WC_Name_Your_Price_Helpers::get_suggested_price( $product_id );
}
$min_price = \WC_Name_Your_Price_Helpers::get_minimum_price( $product_id );
$max_price = \WC_Name_Your_Price_Helpers::get_maximum_price( $product_id );
$price = ! empty( $max_price ) ? min( $price, $max_price ) : $price;
$price = ! empty( $min_price ) ? max( $price, $min_price ) : $price;
$cart_item_data['nyp'] = (float) \WC_Name_Your_Price_Helpers::standardize_number( $price );
}

$cart_item_data['nyp'] = (float) \WC_Name_Your_Price_Helpers::standardize_number( $price );
}
/**
* Filters the cart item data for the modal checkout.
*
* @param array $cart_item_data Cart item data.
*/
$cart_item_data = apply_filters( 'newspack_blocks_modal_checkout_cart_item_data', $cart_item_data );

/**
* Filters the cart item data for the modal checkout.
*
* @param array $cart_item_data Cart item data.
*/
$cart_item_data = apply_filters( 'newspack_blocks_modal_checkout_cart_item_data', $cart_item_data );
\WC()->cart->empty_cart();
\WC()->cart->add_to_cart( $product_id, 1, 0, [], $cart_item_data );

\WC()->cart->empty_cart();
\WC()->cart->add_to_cart( $product_id, 1, 0, [], $cart_item_data );
$query_args = [];
if ( ! empty( $referer_tags ) ) {
$query_args['referer_tags'] = implode( ',', $referer_tags );
}
if ( ! empty( $referer_categories ) ) {
$query_args['referer_categories'] = implode( ',', $referer_categories );
}
$query_args['modal_checkout'] = 1;

$query_args = [];
if ( ! empty( $referer_tags ) ) {
$query_args['referer_tags'] = implode( ',', $referer_tags );
// Pass through UTM and after_success params so they can be forwarded to the WooCommerce checkout flow.
foreach ( $params as $param => $value ) {
if ( 'utm' === substr( $param, 0, 3 ) || 'after_success' === substr( $param, 0, 13 ) ) {
$param = sanitize_text_field( $param );
$query_args[ $param ] = sanitize_text_field( $value );
}
if ( ! empty( $referer_categories ) ) {
$query_args['referer_categories'] = implode( ',', $referer_categories );
}
$query_args['modal_checkout'] = 1;
}

// Pass through UTM and after_success params so they can be forwarded to the WooCommerce checkout flow.
foreach ( $params as $param => $value ) {
if ( 'utm' === substr( $param, 0, 3 ) || 'after_success' === substr( $param, 0, 13 ) ) {
$param = sanitize_text_field( $param );
$query_args[ $param ] = sanitize_text_field( $value );
}
}
$checkout_url = add_query_arg(
$query_args,
\wc_get_page_permalink( 'checkout' )
);

$checkout_url = add_query_arg(
$query_args,
\wc_get_page_permalink( 'checkout' )
);
// Get data to send for this purchase.
$checkout_button_metadata = [
'currency' => function_exists( 'get_woocommerce_currency' ) ? \get_woocommerce_currency() : 'USD',
'amount' => $price,
'product_id' => $product_id,
'referrer' => $referer,
];

// Get data to send for this purchase.
$checkout_button_metadata = [
'currency' => function_exists( 'get_woocommerce_currency' ) ? \get_woocommerce_currency() : 'USD',
'amount' => $price,
'product_id' => $product_id,
'referrer' => $referer,
];
/**
* Action to fire for checkout button block modal.
*/
\do_action( 'newspack_blocks_checkout_button_modal', $checkout_button_metadata );

/**
* Action to fire for checkout button block modal.
*/
\do_action( 'newspack_blocks_checkout_button_modal', $checkout_button_metadata );
$checkout_url = apply_filters( 'newspack_blocks_checkout_url', $checkout_url );

if ( defined( 'DOING_AJAX' ) ) {
echo wp_json_encode( [ 'url' => $checkout_url ] );
exit;
} else {
// Redirect to checkout.
\wp_safe_redirect( apply_filters( 'newspack_blocks_checkout_url', $checkout_url ) );
\wp_safe_redirect( $checkout_url );
exit;
}
}
Expand Down Expand Up @@ -574,7 +589,7 @@ public static function enqueue_scripts() {
return;
}

$dependencies = [ 'jquery' ];
$dependencies = [ 'jquery', 'wc-checkout' ];
// Add support reCAPTCHA dependencies, if connected.
if ( class_exists( 'Newspack\Recaptcha' ) && \Newspack\Recaptcha::can_use_captcha() ) {
$dependencies[] = \Newspack\Recaptcha::SCRIPT_HANDLE;
Expand Down Expand Up @@ -622,9 +637,61 @@ public static function dequeue_scripts() {
if ( ! self::is_modal_checkout() ) {
return;
}
wp_dequeue_style( 'cmplz-general' );
wp_deregister_script( 'wp-mediaelement' );
wp_deregister_style( 'wp-mediaelement' );

$allowed_assets = [
// WP.
'jquery',
// Newspack.
'newspack-newsletters-',
'newspack-blocks-modal',
'newspack-blocks-modal-checkout',
'newspack-wc',
'newspack-ui',
'newspack-style',
'newspack-recaptcha',
// Woo.
'woocommerce',
'WCPAY',
'Woo',
'wc-',
'wc_',
Comment on lines +656 to +657
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Its not uncommon for woo extensions to utilize these prefixes for scripts, so we probably wouldn't be fully covered. That said this is definitely a more thorough approach and better than what we have now.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we talked about making this filterable so that publishers can have other payment gateways if they'd like.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right! Thank you for reminding me, updated in 893a9c4

'wcs-',
'stripe',
];

/**
* Filters the allowed assets to render in the modal checkout
*
* @param string[] $allowed_assets Array of allowed assets handles.
*/
$allowed_assets = apply_filters( 'newspack_blocks_modal_checkout_allowed_assets', $allowed_assets );

global $wp_scripts, $wp_styles;

foreach ( $wp_scripts->queue as $handle ) {
$allowed = false;
foreach ( $allowed_assets as $allowed_asset ) {
if ( false !== strpos( $handle, $allowed_asset ) ) {
$allowed = true;
break;
}
}
if ( ! $allowed ) {
wp_dequeue_script( $handle );
}
}
foreach ( $wp_styles->queue as $handle ) {
$allowed = false;
foreach ( $allowed_assets as $allowed_asset ) {
if ( false !== strpos( $handle, $allowed_asset ) ) {
$allowed = true;
break;
}
}
if ( ! $allowed ) {
wp_dequeue_style( $handle );
}
}
}

/**
Expand All @@ -651,6 +718,7 @@ public static function enqueue_modal( $product_id = null ) {
'newspack-blocks-modal',
'newspackBlocksModal',
[
'ajax_url' => admin_url( 'admin-ajax.php' ),
'checkout_registration_flag' => self::CHECKOUT_REGISTRATION_FLAG,
'newspack_class_prefix' => self::get_class_prefix(),
'labels' => [
Expand Down
Loading