From e48e12c2b8493cb9857886e1777faa8d098a9598 Mon Sep 17 00:00:00 2001 From: Miguel Peixe Date: Tue, 8 Oct 2024 12:27:29 -0300 Subject: [PATCH 01/20] fix(modal-checkout): optimize iframe load --- includes/class-modal-checkout.php | 52 +++++++++- src/modal-checkout/modal.js | 154 ++++++++++++++++++++---------- 2 files changed, 152 insertions(+), 54 deletions(-) diff --git a/includes/class-modal-checkout.php b/includes/class-modal-checkout.php index 7df1fdb74..d9e5c5f8f 100644 --- a/includes/class-modal-checkout.php +++ b/includes/class-modal-checkout.php @@ -88,7 +88,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' ] ); @@ -622,9 +622,53 @@ 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-blocks-modal', + 'newspack-blocks-modal-checkout', + 'newspack-wc', + 'newspack-ui', + 'newspack-style', + 'newspack-recaptcha', + // Woo. + 'woocommerce', + 'WCPAY', + 'Woo', + 'wc-', + 'wc_', + 'wcs-', + 'stripe', + ]; + + 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 ); + } + } } /** diff --git a/src/modal-checkout/modal.js b/src/modal-checkout/modal.js index 6ffd4f557..c99629d5a 100644 --- a/src/modal-checkout/modal.js +++ b/src/modal-checkout/modal.js @@ -39,6 +39,107 @@ domReady( () => { iframe.style.height = initialHeight; iframe.style.visibility = 'hidden'; + function iframeReady( cb ) { + let timer; + let fired = false; + + function ready() { + if ( ! fired ) { + fired = true; + clearTimeout( timer ); + cb.call( this ); + } + } + function readyState() { + if ( this.readyState === "complete" ) { + ready.call( this ); + } + } + function addEvent( elem, event, fn ) { + if ( elem.addEventListener ) { + return elem.addEventListener( event, fn ); + } + return elem.attachEvent( 'on' + event, function () { + return fn.call( elem, window.event ); + } ); + } + function checkLoaded() { + const doc = iframe.contentDocument || iframe.contentWindow?.document; + if ( doc && doc.URL.indexOf('about:') !== 0 ) { + if ( doc?.readyState === 'complete' ) { + ready.call( doc ); + } else { + addEvent( doc, 'DOMContentLoaded', ready ); + addEvent( doc, 'readystatechange', readyState ); + } + } else { + timer = setTimeout( checkLoaded, 10 ); + } + } + checkLoaded(); + } + + /** + * Handle iframe load state. + */ + function handleIframeReady() { + if ( iframe._ready ) { + return; + } + const location = iframe.contentWindow.location; + // If RAS is available, set the front-end authentication. + if ( window.newspackReaderActivation && location.href.indexOf( 'order-received' ) > -1 ) { + const ras = window.newspackReaderActivation; + const params = new Proxy( new URLSearchParams( location.search ), { + get: ( searchParams, prop ) => searchParams.get( prop ), + } ); + if ( params.email ) { + ras.setReaderEmail( params.email ); + ras.setAuthenticated( true ); + } + } + const container = iframe?.contentDocument?.querySelector( `#${ IFRAME_CONTAINER_ID }` ); + const setModalReady = () => { + if ( iframe._ready ) { + return; + } + iframeResizeObserver.observe( container ); + if ( spinner.style.display !== 'none' ) { + spinner.style.display = 'none'; + } + if ( iframe.style.visibility !== 'visible' ) { + iframe.style.visibility = 'visible'; + } + iframe._ready = true; + } + if ( container ) { + if ( container.checkoutComplete ) { + // Update the modal title and width to reflect successful transaction. + setModalSize( 'small' ); + setModalTitle( newspackBlocksModal.labels.thankyou_modal_title ); + setModalReady(); + a11y.trapFocus( modalCheckout.querySelector( `.${ MODAL_CLASS_PREFIX }` ) ); + } else { + // Revert modal title and width default value. + setModalSize(); + setModalTitle( newspackBlocksModal.labels.checkout_modal_title ); + } + if ( container.checkoutReady ) { + setModalReady(); + } else { + container.addEventListener( 'checkout-ready', setModalReady ); + } + // Make sure the iframe has actually loaded something, even if not the expected container. + // This check prevents an issue in Chrome where the 'load' event fired twice and the spinner was hidden too soon. + } else if ( 'about:blank' !== location.href ) { + setModalReady(); + } + } + + iframe.addEventListener( 'load', () => { + handleIframeReady(); + } ); + const iframeResizeObserver = new ResizeObserver( entries => { if ( ! entries || ! entries.length ) { return; @@ -125,6 +226,7 @@ domReady( () => { manageDismissed(); document.getElementById( 'newspack_modal_checkout' ).removeAttribute( 'data-order-details' ); } + iframe._ready = false; }; const openCheckout = () => { @@ -138,6 +240,8 @@ domReady( () => { } ); a11y.trapFocus( modalCheckout, iframe ); + + iframeReady( handleIframeReady ); }; const closeModal = el => { @@ -505,56 +609,6 @@ domReady( () => { } ); } ); - /** - * Handle iframe load state. - */ - iframe.addEventListener( 'load', () => { - const location = iframe.contentWindow.location; - // If RAS is available, set the front-end authentication. - if ( window.newspackReaderActivation && location.href.indexOf( 'order-received' ) > -1 ) { - const ras = window.newspackReaderActivation; - const params = new Proxy( new URLSearchParams( location.search ), { - get: ( searchParams, prop ) => searchParams.get( prop ), - } ); - if ( params.email ) { - ras.setReaderEmail( params.email ); - ras.setAuthenticated( true ); - } - } - const container = iframe?.contentDocument?.querySelector( `#${ IFRAME_CONTAINER_ID }` ); - const setModalReady = () => { - iframeResizeObserver.observe( container ); - if ( spinner.style.display !== 'none' ) { - spinner.style.display = 'none'; - } - if ( iframe.style.visibility !== 'visible' ) { - iframe.style.visibility = 'visible'; - } - } - if ( container ) { - if ( container.checkoutComplete ) { - // Update the modal title and width to reflect successful transaction. - setModalSize( 'small' ); - setModalTitle( newspackBlocksModal.labels.thankyou_modal_title ); - setModalReady(); - a11y.trapFocus( modalCheckout.querySelector( `.${ MODAL_CLASS_PREFIX }` ) ); - } else { - // Revert modal title and width default value. - setModalSize(); - setModalTitle( newspackBlocksModal.labels.checkout_modal_title ); - } - if ( container.checkoutReady ) { - setModalReady(); - } else { - container.addEventListener( 'checkout-ready', () => setModalReady() ); - } - // Make sure the iframe has actually loaded something, even if not the expected container. - // This check prevents an issue in Chrome where the 'load' event fired twice and the spinner was hidden too soon. - } else if ( 'about:blank' !== location.href ) { - setModalReady(); - } - } ); - /** * Triggers checkout form submit. * From f3f6992e747660f39bed8f8216648d4e337ad65a Mon Sep 17 00:00:00 2001 From: Miguel Peixe Date: Tue, 8 Oct 2024 12:30:55 -0300 Subject: [PATCH 02/20] fix: prevent multiple runners --- src/modal-checkout/modal.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/modal-checkout/modal.js b/src/modal-checkout/modal.js index c99629d5a..0ec508f09 100644 --- a/src/modal-checkout/modal.js +++ b/src/modal-checkout/modal.js @@ -40,13 +40,15 @@ domReady( () => { iframe.style.visibility = 'hidden'; function iframeReady( cb ) { - let timer; + if ( iframe._readyTimer ) { + clearTimeout( iframe._readyTimer ); + } let fired = false; function ready() { if ( ! fired ) { fired = true; - clearTimeout( timer ); + clearTimeout( iframe._readyTimer ); cb.call( this ); } } @@ -73,7 +75,7 @@ domReady( () => { addEvent( doc, 'readystatechange', readyState ); } } else { - timer = setTimeout( checkLoaded, 10 ); + iframe._readyTimer = setTimeout( checkLoaded, 10 ); } } checkLoaded(); From f0972b7345e95b11b11365500e162b0225062de2 Mon Sep 17 00:00:00 2001 From: Miguel Peixe Date: Tue, 8 Oct 2024 14:54:39 -0300 Subject: [PATCH 03/20] fix: allow `newspack-newsletters-` scripts --- includes/class-modal-checkout.php | 1 + 1 file changed, 1 insertion(+) diff --git a/includes/class-modal-checkout.php b/includes/class-modal-checkout.php index d9e5c5f8f..f43e16354 100644 --- a/includes/class-modal-checkout.php +++ b/includes/class-modal-checkout.php @@ -627,6 +627,7 @@ public static function dequeue_scripts() { // WP. 'jquery', // Newspack. + 'newspack-newsletters-', 'newspack-blocks-modal', 'newspack-blocks-modal-checkout', 'newspack-wc', From abb28ac2f1fdf6e2fc8ffb769f70e6ae069bb059 Mon Sep 17 00:00:00 2001 From: Miguel Peixe Date: Tue, 8 Oct 2024 14:55:10 -0300 Subject: [PATCH 04/20] fix: call cb on early return and log missing jquery --- src/modal-checkout/index.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/modal-checkout/index.js b/src/modal-checkout/index.js index af43ddb5c..c484de3ce 100644 --- a/src/modal-checkout/index.js +++ b/src/modal-checkout/index.js @@ -13,6 +13,7 @@ import { domReady } from './utils'; domReady( ( $ => { if ( ! $ ) { + console.warn( 'jQuery is not available.' ); // eslint-disable-line no-console return; } @@ -587,6 +588,7 @@ domReady( function validateForm( silent = false, cb = () => {} ) { const blocked = blockForm( $form ); if ( ! blocked ) { + cb(); return false; } clearNotices(); From 07f5df3fa240b68d13f21dbe72f7698d03a875be Mon Sep 17 00:00:00 2001 From: Miguel Peixe Date: Tue, 8 Oct 2024 15:16:37 -0300 Subject: [PATCH 05/20] chore: log when unable to block form --- src/modal-checkout/index.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/modal-checkout/index.js b/src/modal-checkout/index.js index c484de3ce..e050ecac0 100644 --- a/src/modal-checkout/index.js +++ b/src/modal-checkout/index.js @@ -588,6 +588,7 @@ domReady( function validateForm( silent = false, cb = () => {} ) { const blocked = blockForm( $form ); if ( ! blocked ) { + console.warn( 'Unable to block the form' ); // eslint-disable-line no-console cb(); return false; } From f55c4bb4a739af8d4a6baaf1812fac0635892745 Mon Sep 17 00:00:00 2001 From: Miguel Peixe Date: Tue, 8 Oct 2024 15:18:23 -0300 Subject: [PATCH 06/20] fix: fix iframe ready check and remove ie8 support --- src/modal-checkout/modal.js | 29 ++++++++--------------------- 1 file changed, 8 insertions(+), 21 deletions(-) diff --git a/src/modal-checkout/modal.js b/src/modal-checkout/modal.js index 0ec508f09..f56281e7c 100644 --- a/src/modal-checkout/modal.js +++ b/src/modal-checkout/modal.js @@ -56,23 +56,18 @@ domReady( () => { if ( this.readyState === "complete" ) { ready.call( this ); } - } - function addEvent( elem, event, fn ) { - if ( elem.addEventListener ) { - return elem.addEventListener( event, fn ); + } function checkLoaded() { + if ( iframe._ready ) { + clearTimeout( iframe._readyTimer ); + return; } - return elem.attachEvent( 'on' + event, function () { - return fn.call( elem, window.event ); - } ); - } - function checkLoaded() { const doc = iframe.contentDocument || iframe.contentWindow?.document; if ( doc && doc.URL.indexOf('about:') !== 0 ) { if ( doc?.readyState === 'complete' ) { ready.call( doc ); } else { - addEvent( doc, 'DOMContentLoaded', ready ); - addEvent( doc, 'readystatechange', readyState ); + doc.addEventListener( 'DOMContentLoaded', ready ); + doc.addEventListener( 'readystatechange', readyState ); } } else { iframe._readyTimer = setTimeout( checkLoaded, 10 ); @@ -85,9 +80,6 @@ domReady( () => { * Handle iframe load state. */ function handleIframeReady() { - if ( iframe._ready ) { - return; - } const location = iframe.contentWindow.location; // If RAS is available, set the front-end authentication. if ( window.newspackReaderActivation && location.href.indexOf( 'order-received' ) > -1 ) { @@ -102,9 +94,6 @@ domReady( () => { } const container = iframe?.contentDocument?.querySelector( `#${ IFRAME_CONTAINER_ID }` ); const setModalReady = () => { - if ( iframe._ready ) { - return; - } iframeResizeObserver.observe( container ); if ( spinner.style.display !== 'none' ) { spinner.style.display = 'none'; @@ -138,9 +127,7 @@ domReady( () => { } } - iframe.addEventListener( 'load', () => { - handleIframeReady(); - } ); + iframe.addEventListener( 'load', handleIframeReady ); const iframeResizeObserver = new ResizeObserver( entries => { if ( ! entries || ! entries.length ) { @@ -174,6 +161,7 @@ domReady( () => { spinner.style.display = 'flex'; if ( iframe && modalContent.contains( iframe ) ) { // Reset iframe and modal content heights. + iframe._ready = false; iframe.src = 'about:blank'; iframe.style.height = initialHeight; iframe.style.visibility = 'hidden'; @@ -228,7 +216,6 @@ domReady( () => { manageDismissed(); document.getElementById( 'newspack_modal_checkout' ).removeAttribute( 'data-order-details' ); } - iframe._ready = false; }; const openCheckout = () => { From 25f6118f9e95b507e410f721ab4a98a4d5269c3a Mon Sep 17 00:00:00 2001 From: Miguel Peixe Date: Tue, 8 Oct 2024 18:43:46 -0300 Subject: [PATCH 07/20] fix: remove `init_checkout` listener --- includes/class-modal-checkout.php | 2 +- src/modal-checkout/index.js | 7 ++++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/includes/class-modal-checkout.php b/includes/class-modal-checkout.php index f43e16354..e24c3cd63 100644 --- a/includes/class-modal-checkout.php +++ b/includes/class-modal-checkout.php @@ -574,7 +574,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; diff --git a/src/modal-checkout/index.js b/src/modal-checkout/index.js index e050ecac0..d3b9b845b 100644 --- a/src/modal-checkout/index.js +++ b/src/modal-checkout/index.js @@ -59,8 +59,7 @@ domReady( container.checkoutComplete = true; } } else { - $( document.body ).on( 'init_checkout', function () { - + function init() { // If present, update the markup used for the WooPayments express checkout divider. $( '#wcpay-express-checkout-button-separator, #wc-stripe-payment-request-button-separator' ).after( '
' + newspackBlocksModalCheckout.divider_text + '
' @@ -71,6 +70,7 @@ domReady( const $form = $( 'form.checkout' ); if ( ! $form.length ) { + console.warn( 'Form is not available' ); // eslint-disable-line no-console return; } const $coupon = $( 'form.modal_checkout_coupon' ); @@ -718,7 +718,8 @@ domReady( form.removeClass( 'modal-processing' ); return true; } - } ); + } + init(); } /** From 7dcfc124c9824c791b85ddc64fef1aa979d0cee1 Mon Sep 17 00:00:00 2001 From: Miguel Peixe Date: Wed, 9 Oct 2024 15:32:56 -0300 Subject: [PATCH 08/20] fix: immediately invoked function --- src/modal-checkout/index.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/modal-checkout/index.js b/src/modal-checkout/index.js index d3b9b845b..0f4515e68 100644 --- a/src/modal-checkout/index.js +++ b/src/modal-checkout/index.js @@ -10,8 +10,8 @@ import './checkout.scss'; import { manageCheckoutAttempt, manageLoaded, managePagination } from './analytics'; import { domReady } from './utils'; -domReady( - ( $ => { +( $ => { + domReady( () => { if ( ! $ ) { console.warn( 'jQuery is not available.' ); // eslint-disable-line no-console return; @@ -748,5 +748,5 @@ domReady( parent.newspackCloseModalCheckout(); } } ); - } )( jQuery ) -); + } ) +} )( jQuery ); From 27d3bc460406522a649fb26d1bd084cb5dc4b6b9 Mon Sep 17 00:00:00 2001 From: Miguel Peixe Date: Fri, 11 Oct 2024 12:04:58 -0300 Subject: [PATCH 09/20] feat: add to cart async with the auth flow --- includes/class-modal-checkout.php | 28 ++++++++++++++++++---------- src/modal-checkout/modal.js | 7 +++++++ 2 files changed, 25 insertions(+), 10 deletions(-) diff --git a/includes/class-modal-checkout.php b/includes/class-modal-checkout.php index 9c3f39320..d24210ddc 100644 --- a/includes/class-modal-checkout.php +++ b/includes/class-modal-checkout.php @@ -52,7 +52,11 @@ 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_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' ] ); @@ -151,12 +155,17 @@ public static function dequeue_woocommerce_styles( $enqueue_styles ) { * 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 ); + $is_newspack_donate = filter_input( INPUT_GET, 'newspack_donate', FILTER_SANITIZE_NUMBER_INT ); + + if ( ! $is_newspack_checkout && ! $is_newspack_donate ) { 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 ); $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 ); @@ -164,10 +173,6 @@ public static function process_checkout_request() { $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 ) { - return; - } - // Flag the checkout as a registration. if ( $is_checkout_registration ) { \WC()->session->set( self::CHECKOUT_REGISTRATION_FLAG, true ); @@ -287,9 +292,11 @@ function ( $item ) { */ \do_action( 'newspack_blocks_checkout_button_modal', $checkout_button_metadata ); - // Redirect to checkout. + if ( ! defined( 'DOING_AJAX' ) ) { + // Redirect to checkout. \wp_safe_redirect( apply_filters( 'newspack_blocks_checkout_url', $checkout_url ) ); - exit; + exit; + } } } @@ -696,6 +703,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' => [ diff --git a/src/modal-checkout/modal.js b/src/modal-checkout/modal.js index f56281e7c..4f9323e74 100644 --- a/src/modal-checkout/modal.js +++ b/src/modal-checkout/modal.js @@ -562,6 +562,13 @@ domReady( () => { } window.newspackReaderActivation?.setCheckoutData?.( data ); + // Add to cart asynchroneously. + const urlParams = new URLSearchParams( formData ); + urlParams.append( 'action', 'modal_checkout_request' ); + fetch( newspackBlocksModal.ajax_url + '?' + urlParams.toString() ).catch( error => { + console.warn( 'Unable to generate cart:', error ); // eslint-disable-line no-console + } ); + // Initialize auth flow if reader is not authenticated. window.newspackReaderActivation.openAuthModal( { title: newspackBlocksModal.labels.auth_modal_title, From 12b8f2657e1523fccc97b449e6d7916665f1d816 Mon Sep 17 00:00:00 2001 From: Miguel Peixe Date: Fri, 11 Oct 2024 12:06:20 -0300 Subject: [PATCH 10/20] chore: lint --- includes/class-modal-checkout.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/includes/class-modal-checkout.php b/includes/class-modal-checkout.php index d24210ddc..32151afe1 100644 --- a/includes/class-modal-checkout.php +++ b/includes/class-modal-checkout.php @@ -703,7 +703,7 @@ public static function enqueue_modal( $product_id = null ) { 'newspack-blocks-modal', 'newspackBlocksModal', [ - 'ajax_url' => admin_url( 'admin-ajax.php' ), + 'ajax_url' => admin_url( 'admin-ajax.php' ), 'checkout_registration_flag' => self::CHECKOUT_REGISTRATION_FLAG, 'newspack_class_prefix' => self::get_class_prefix(), 'labels' => [ From 3cfbf80e75ecd969865ab1cac5a38e7911928669 Mon Sep 17 00:00:00 2001 From: Miguel Peixe Date: Fri, 11 Oct 2024 15:37:40 -0300 Subject: [PATCH 11/20] fix: refactor anonymous cart handling --- includes/class-modal-checkout.php | 12 +- src/modal-checkout/modal.js | 694 ++++++++++++++---------------- 2 files changed, 320 insertions(+), 386 deletions(-) diff --git a/includes/class-modal-checkout.php b/includes/class-modal-checkout.php index 32151afe1..22a79a413 100644 --- a/includes/class-modal-checkout.php +++ b/includes/class-modal-checkout.php @@ -160,9 +160,8 @@ public static function process_checkout_request() { } $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 ); - if ( ! $is_newspack_checkout && ! $is_newspack_donate ) { + if ( ! $is_newspack_checkout ) { return; } @@ -292,9 +291,14 @@ function ( $item ) { */ \do_action( 'newspack_blocks_checkout_button_modal', $checkout_button_metadata ); - if ( ! defined( 'DOING_AJAX' ) ) { + $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; } } diff --git a/src/modal-checkout/modal.js b/src/modal-checkout/modal.js index 4f9323e74..477a21cf9 100644 --- a/src/modal-checkout/modal.js +++ b/src/modal-checkout/modal.js @@ -129,6 +129,312 @@ domReady( () => { iframe.addEventListener( 'load', handleIframeReady ); + /** + * Generate cart. + * + * @return {Promise} The promise that resolves with the checkout URL. + */ + const generateCart = ( formData ) => { + return new Promise( ( resolve, reject ) => { + const urlParams = new URLSearchParams( formData ); + urlParams.append( 'action', 'modal_checkout_request' ); + fetch( newspackBlocksModal.ajax_url + '?' + urlParams.toString() ) + .then( res => { + if ( ! res.ok ) { + reject( res ); + } + res.json() + .then( jsonData => { + resolve( jsonData.url ); + } ) + .catch( reject ); + } ) + .catch( reject ); + } ); + } + + /** + * Handle checkout form submit. + * + * @param {Event} ev + */ + const handleCheckoutFormSubmit = ev => { + const form = ev.target; + + form.classList.add( 'modal-processing' ); + + const productData = form.dataset.product; + + if ( productData ) { + const data = JSON.parse( productData ); + Object.keys( data ).forEach( key => { + const existingInputs = form.querySelectorAll( 'input[name="' + key + '"]' ); + if ( 0 === existingInputs.length ) { + form.appendChild( createHiddenInput( key, data[ key ] ) ); + } + } ); + } + const formData = new FormData( form ); + + // If we're not going from variation picker to checkout, set the modal trigger: + if ( ! formData.get( 'variation_id' ) ) { + modalTrigger = ev.submitter; + } + + const variationModals = document.querySelectorAll( `.${ VARIATON_MODAL_CLASS_PREFIX }` ); + // Clear any open variation modal. + variationModals.forEach( variationModal => { + closeModal( variationModal ); + } ); + // Trigger variation modal if variation is not selected. + if ( formData.get( 'is_variable' ) && ! formData.get( 'variation_id' ) ) { + const variationModal = [ ...variationModals ].find( + modal => modal.dataset.productId === formData.get( 'product_id' ) + ); + if ( variationModal ) { + variationModal + .querySelectorAll( `form[target="${ IFRAME_NAME }"]` ) + .forEach( singleVariationForm => { + // Fill in the after success variables in the variation modal. + [ + 'after_success_behavior', + 'after_success_url', + 'after_success_button_label', + ].forEach( afterSuccessParam => { + const existingInputs = singleVariationForm.querySelectorAll( 'input[name="' + afterSuccessParam + '"]' ); + if ( 0 === existingInputs.length ) { + singleVariationForm.appendChild( createHiddenInput( afterSuccessParam, formData.get( afterSuccessParam ) ) ); + } + } ); + + // Append the product data hidden inputs. + const variationData = singleVariationForm.dataset.product; + if ( variationData ) { + const data = JSON.parse( variationData ); + Object.keys( data ).forEach( key => { + const existingInputs = singleVariationForm.querySelectorAll( 'input[name="' + key + '"]' ); + if ( 0 === existingInputs.length ) { + singleVariationForm.appendChild( createHiddenInput( key, data[ key ] ) ); + } + } ); + } + } ); + + // Open the variations modal. + ev.preventDefault(); + form.classList.remove( 'modal-processing' ); + openModal( variationModal ); + a11y.trapFocus( variationModal, false ); + + // Set up some GA4 information. + const getDataProduct = form.getAttribute( 'data-product' ); + getProductDataModal = getDataProduct ? JSON.parse( getDataProduct ) : {}; + manageOpened( getProductDataModal ); + + // Append product data info to the modal itself, so we can grab it for manageDismissed: + document + .getElementById( 'newspack_modal_checkout' ) + .setAttribute( 'data-order-details', JSON.stringify( getProductDataModal ) ); + return; + } + } + + form.classList.remove( 'modal-processing' ); + + const isDonateBlock = formData.get( 'newspack_donate' ); + const isCheckoutButtonBlock = formData.get( 'newspack_checkout' ); + + // Set up some GA4 information. + if ( isCheckoutButtonBlock ) { // this fires on the second in-modal variations screen, too + const getDataProduct = form.getAttribute( 'data-product' ); + getProductDataModal = getDataProduct ? JSON.parse( getDataProduct ) : {}; + } else if ( isDonateBlock ) { + // Get donation information and append to the modal checkout for GA4: + const donationFreq = formData.get( 'donation_frequency' ); + let donationValue = ''; + let productId = ''; + + for ( const key of formData.keys() ) { + // Find values that match the frequency name, that aren't empty + if ( + key.indexOf( 'donation_value_' + donationFreq ) >= 0 && + 'other' !== formData.get( key ) && + '' !== formData.get( key ) + ) { + donationValue = formData.get( key ); + } + } + + // Get IDs for donation frequencies, and compare them to the selected frequency. + const freqIds = JSON.parse( formData.get( 'frequency_ids' ) ); + for ( const freq in freqIds ) { + if ( freq === donationFreq ) { + productId = freqIds[freq].toString(); + } + } + + // Get product information together to be appended to the modal for GA4 events outside of the iframe. + getProductDataModal = { + amount: donationValue, + action_type: 'donation', + currency: formData.get( 'donation_currency' ), + product_id: productId, + product_type: 'donation', + recurrence: donationFreq, + referrer: formData.get( '_wp_http_referer' ), + }; + } + + if ( + typeof newspack_ras_config !== 'undefined' && + ! newspack_ras_config?.is_logged_in && + ! window?.newspackReaderActivation?.getReader?.()?.authenticated && + window?.newspackReaderActivation?.openAuthModal + ) { + ev.preventDefault(); + let content = ''; + let price = '0'; + let priceSummary = ''; + + if ( isDonateBlock ) { + const frequency = formData.get( 'donation_frequency' ); + const donationTiers = form.querySelectorAll( + `.donation-tier__${ frequency }, .donation-frequency__${ frequency }` + ); + + if ( donationTiers?.length ) { + const frequencyInputs = form.querySelectorAll( + `input[name="donation_value_${ frequency }"], input[name="donation_value_${ frequency }_untiered"]` + ); + + if ( frequencyInputs?.length ) { + // Handle frequency based donation tiers. + frequencyInputs.forEach( input => { + if ( input.checked && input.value !== 'other' ) { + price = input.value; + } + } ); + + donationTiers.forEach( el => { + const donationData = JSON.parse( el.dataset.product ); + if ( + donationData.hasOwnProperty( `donation_price_summary_${ frequency }` ) && + donationData?.[ `donation_price_summary_${ frequency }` ].includes( price ) + ) { + priceSummary = donationData[ `donation_price_summary_${ frequency }` ]; + } + + if ( price === '0' && priceSummary ) { + // Replace placeholder price with price input for other. + let otherPrice = formData.get( `donation_value_${ frequency }_other` ); + + // Fallback to untiered price if other price is not set. + if ( ! otherPrice ) { + otherPrice = formData.get( `donation_value_${ frequency }_untiered` ); + } + + if ( otherPrice ) { + priceSummary = priceSummary.replace( '0', otherPrice ); + } + } + } ); + } else { + // Handle tiers based donation tiers. + const index = formData.get( 'donation_tier_index' ); + if ( index ) { + const donationData = JSON.parse( donationTiers?.[ index ].dataset.product ); + if ( donationData.hasOwnProperty( `donation_price_summary_${ frequency }` ) ) { + priceSummary = donationData[ `donation_price_summary_${ frequency }` ]; + } + } + } + } + } else if ( isCheckoutButtonBlock ) { + const priceSummaryInput = form.querySelector( 'input[name="product_price_summary"]' ); + + if ( priceSummaryInput ) { + priceSummary = priceSummaryInput.value; + } + } + + if ( priceSummary ) { + content = `

${ priceSummary }

`; + } + + // Generate cart asynchroneously. + const cartReq = generateCart( formData ); + + // Update pending checkout URL. + cartReq.then( url => { + window.newspackReaderActivation?.setPendingCheckout?.( url ); + } ); + + // Initialize auth flow if reader is not authenticated. + window.newspackReaderActivation.openAuthModal( { + title: newspackBlocksModal.labels.auth_modal_title, + callback: ( message, authData ) => { + cartReq.then( url => { + const checkoutForm = generateCheckoutPageForm( url ); + // Signal checkout registration. + if ( authData?.registered ) { + checkoutForm.appendChild( + createHiddenInput( newspackBlocksModal.checkout_registration_flag, '1' ) + ); + } + triggerCheckout( checkoutForm ); + } ) + .catch( error => { + console.warn( 'Unable to generate cart:', error ); // eslint-disable-line no-console + } ); + }, + skipSuccess: true, + skipNewslettersSignup: true, + labels: { + signin: { + title: newspackBlocksModal.labels.signin_modal_title, + }, + register: { + title: newspackBlocksModal.labels.register_modal_title, + }, + }, + content, + trigger: ev.submitter, + } ); + } else { + // Otherwise initialize checkout. + openCheckout(); + manageOpened( getProductDataModal ); + // Append product data info to the modal, so we can grab it for GA4 events outside of the iframe. + document + .getElementById( 'newspack_modal_checkout' ) + .setAttribute( 'data-order-details', JSON.stringify( getProductDataModal ) ); + } + }; + + /** + * Generate checkout page form. + * + * A form that goes directly to checkout in case the cart has already been + * created. + */ + const generateCheckoutPageForm = checkoutUrl => { + const checkoutForm = document.createElement( 'form' ); + checkoutForm.method = 'POST'; + checkoutForm.action = checkoutUrl; + checkoutForm.target = IFRAME_NAME; + checkoutForm.style.display = 'none'; + + const submitButton = document.createElement( 'button' ); + submitButton.setAttribute( 'type', 'submit' ); + + checkoutForm.appendChild( submitButton ); + document.body.appendChild( checkoutForm ); + + checkoutForm.addEventListener( 'submit', handleCheckoutFormSubmit ); + + return checkoutForm; + } + const iframeResizeObserver = new ResizeObserver( entries => { if ( ! entries || ! entries.length ) { return; @@ -331,277 +637,7 @@ domReady( () => { forms.forEach( form => { form.appendChild( modalCheckoutHiddenInput.cloneNode() ); form.target = IFRAME_NAME; - form.addEventListener( 'submit', ev => { - form.classList.add( 'modal-processing' ); - - const productData = form.dataset.product; - - if ( productData ) { - const data = JSON.parse( productData ); - Object.keys( data ).forEach( key => { - const existingInputs = form.querySelectorAll( 'input[name="' + key + '"]' ); - if ( 0 === existingInputs.length ) { - form.appendChild( createHiddenInput( key, data[ key ] ) ); - } - } ); - } - const formData = new FormData( form ); - - // If we're not going from variation picker to checkout, set the modal trigger: - if ( ! formData.get( 'variation_id' ) ) { - modalTrigger = ev.submitter; - } - - const variationModals = document.querySelectorAll( `.${ VARIATON_MODAL_CLASS_PREFIX }` ); - // Clear any open variation modal. - variationModals.forEach( variationModal => { - closeModal( variationModal ); - } ); - // Trigger variation modal if variation is not selected. - if ( formData.get( 'is_variable' ) && ! formData.get( 'variation_id' ) ) { - const variationModal = [ ...variationModals ].find( - modal => modal.dataset.productId === formData.get( 'product_id' ) - ); - if ( variationModal ) { - variationModal - .querySelectorAll( `form[target="${ IFRAME_NAME }"]` ) - .forEach( singleVariationForm => { - // Fill in the after success variables in the variation modal. - [ - 'after_success_behavior', - 'after_success_url', - 'after_success_button_label', - ].forEach( afterSuccessParam => { - const existingInputs = singleVariationForm.querySelectorAll( 'input[name="' + afterSuccessParam + '"]' ); - if ( 0 === existingInputs.length ) { - singleVariationForm.appendChild( createHiddenInput( afterSuccessParam, formData.get( afterSuccessParam ) ) ); - } - } ); - - // Append the product data hidden inputs. - const variationData = singleVariationForm.dataset.product; - if ( variationData ) { - const data = JSON.parse( variationData ); - Object.keys( data ).forEach( key => { - const existingInputs = singleVariationForm.querySelectorAll( 'input[name="' + key + '"]' ); - if ( 0 === existingInputs.length ) { - singleVariationForm.appendChild( createHiddenInput( key, data[ key ] ) ); - } - } ); - } - } ); - - // Open the variations modal. - ev.preventDefault(); - form.classList.remove( 'modal-processing' ); - openModal( variationModal ); - a11y.trapFocus( variationModal, false ); - - // Set up some GA4 information. - const getDataProduct = form.getAttribute( 'data-product' ); - getProductDataModal = getDataProduct ? JSON.parse( getDataProduct ) : {}; - manageOpened( getProductDataModal ); - - // Append product data info to the modal itself, so we can grab it for manageDismissed: - document - .getElementById( 'newspack_modal_checkout' ) - .setAttribute( 'data-order-details', JSON.stringify( getProductDataModal ) ); - return; - } - } - - form.classList.remove( 'modal-processing' ); - - const isDonateBlock = formData.get( 'newspack_donate' ); - const isCheckoutButtonBlock = formData.get( 'newspack_checkout' ); - - // Set up some GA4 information. - if ( isCheckoutButtonBlock ) { // this fires on the second in-modal variations screen, too - const getDataProduct = form.getAttribute( 'data-product' ); - getProductDataModal = getDataProduct ? JSON.parse( getDataProduct ) : {}; - } else if ( isDonateBlock ) { - // Get donation information and append to the modal checkout for GA4: - const donationFreq = formData.get( 'donation_frequency' ); - let donationValue = ''; - let productId = ''; - - for ( const key of formData.keys() ) { - // Find values that match the frequency name, that aren't empty - if ( - key.indexOf( 'donation_value_' + donationFreq ) >= 0 && - 'other' !== formData.get( key ) && - '' !== formData.get( key ) - ) { - donationValue = formData.get( key ); - } - } - - // Get IDs for donation frequencies, and compare them to the selected frequency. - const freqIds = JSON.parse( formData.get( 'frequency_ids' ) ); - for ( const freq in freqIds ) { - if ( freq === donationFreq ) { - productId = freqIds[freq].toString(); - } - } - - // Get product information together to be appended to the modal for GA4 events outside of the iframe. - getProductDataModal = { - amount: donationValue, - action_type: 'donation', - currency: formData.get( 'donation_currency' ), - product_id: productId, - product_type: 'donation', - recurrence: donationFreq, - referrer: formData.get( '_wp_http_referer' ), - }; - } - - if ( - typeof newspack_ras_config !== 'undefined' && - ! newspack_ras_config?.is_logged_in && - ! window?.newspackReaderActivation?.getReader?.()?.authenticated && - window?.newspackReaderActivation?.openAuthModal - ) { - ev.preventDefault(); - let content = ''; - let price = '0'; - let priceSummary = ''; - - if ( isDonateBlock ) { - const frequency = formData.get( 'donation_frequency' ); - const donationTiers = form.querySelectorAll( - `.donation-tier__${ frequency }, .donation-frequency__${ frequency }` - ); - - if ( donationTiers?.length ) { - const frequencyInputs = form.querySelectorAll( - `input[name="donation_value_${ frequency }"], input[name="donation_value_${ frequency }_untiered"]` - ); - - if ( frequencyInputs?.length ) { - // Handle frequency based donation tiers. - frequencyInputs.forEach( input => { - if ( input.checked && input.value !== 'other' ) { - price = input.value; - } - } ); - - donationTiers.forEach( el => { - const donationData = JSON.parse( el.dataset.product ); - if ( - donationData.hasOwnProperty( `donation_price_summary_${ frequency }` ) && - donationData?.[ `donation_price_summary_${ frequency }` ].includes( price ) - ) { - priceSummary = donationData[ `donation_price_summary_${ frequency }` ]; - } - - if ( price === '0' && priceSummary ) { - // Replace placeholder price with price input for other. - let otherPrice = formData.get( `donation_value_${ frequency }_other` ); - - // Fallback to untiered price if other price is not set. - if ( ! otherPrice ) { - otherPrice = formData.get( `donation_value_${ frequency }_untiered` ); - } - - if ( otherPrice ) { - priceSummary = priceSummary.replace( '0', otherPrice ); - } - } - } ); - } else { - // Handle tiers based donation tiers. - const index = formData.get( 'donation_tier_index' ); - if ( index ) { - const donationData = JSON.parse( donationTiers?.[ index ].dataset.product ); - if ( donationData.hasOwnProperty( `donation_price_summary_${ frequency }` ) ) { - priceSummary = donationData[ `donation_price_summary_${ frequency }` ]; - } - } - } - } - } else if ( isCheckoutButtonBlock ) { - const priceSummaryInput = form.querySelector( 'input[name="product_price_summary"]' ); - - if ( priceSummaryInput ) { - priceSummary = priceSummaryInput.value; - } - } - - if ( priceSummary ) { - content = `

${ priceSummary }

`; - } - - // Set reader activation checkout data if available. - const data = {}; - if ( element.classList.contains( 'wpbnbd--platform-wc' ) ) { - const frequency = formData.get( 'donation_frequency' ); - let amount; - let layout; - if ( formData.has( `donation_value_${ frequency }_untiered` ) ) { - amount = formData.get( `donation_value_${ frequency }_untiered` ); - layout = 'untiered'; - } else if ( formData.has( 'donation_tier_index' ) ) { - const donationTier = form.querySelector( `button[data-tier-index="${ formData.get('donation_tier_index') }"]` ); - amount = donationTier?.value; - layout = 'tiered'; - } else { - amount = formData.get( `donation_value_${ frequency }` ) - layout = 'frequency'; - } - - data.type = 'donate'; - data.layout = layout; - data.frequency = frequency; - data.amount = amount; - data.other = formData.get( `donation_value_${ frequency }_other` ); - } else { - data.type = 'checkout_button'; - data.product_id = formData.get( 'product_id' ); - data.variation_id = formData.get( 'variation_id' ); - } - window.newspackReaderActivation?.setCheckoutData?.( data ); - - // Add to cart asynchroneously. - const urlParams = new URLSearchParams( formData ); - urlParams.append( 'action', 'modal_checkout_request' ); - fetch( newspackBlocksModal.ajax_url + '?' + urlParams.toString() ).catch( error => { - console.warn( 'Unable to generate cart:', error ); // eslint-disable-line no-console - } ); - - // Initialize auth flow if reader is not authenticated. - window.newspackReaderActivation.openAuthModal( { - title: newspackBlocksModal.labels.auth_modal_title, - callback: () => { - // Signal checkout registration. - form.appendChild( - createHiddenInput( newspackBlocksModal.checkout_registration_flag, '1' ) - ); - triggerCheckout( form ); - }, - skipSuccess: true, - skipNewslettersSignup: true, - labels: { - signin: { - title: newspackBlocksModal.labels.signin_modal_title, - }, - register: { - title: newspackBlocksModal.labels.register_modal_title, - }, - }, - content, - trigger: ev.submitter, - } ); - } else { - // Otherwise initialize checkout. - openCheckout(); - manageOpened( getProductDataModal ); - // Append product data info to the modal, so we can grab it for GA4 events outside of the iframe. - document - .getElementById( 'newspack_modal_checkout' ) - .setAttribute( 'data-order-details', JSON.stringify( getProductDataModal ) ); - } - } ); + form.addEventListener( 'submit', handleCheckoutFormSubmit ); } ); } ); @@ -615,124 +651,18 @@ domReady( () => { form.requestSubmit( form.querySelector( 'button[type="submit"]' ) ); } - /** - * Handle donation form triggers. - * - * @param {string} layout The donation layout. - * @param {string} frequency The donation frequency. - * @param {string} amount The donation amount. - * @param {string|null} other Optional. The custom amount when other is selected. - */ - const triggerDonationForm = ( layout, frequency, amount, other = null ) => { - let form; - document.querySelectorAll( '.wpbnbd.wpbnbd--platform-wc form' ) - .forEach( donationForm => { - const frequencyInput = donationForm.querySelector( `input[name="donation_frequency"][value="${ frequency }"]` ); - if ( ! frequencyInput ) { - return; - } - if ( layout === 'tiered' ) { - const frequencyButton = document.querySelector( `button[data-frequency-slug="${ frequency }"]` ); - if ( ! frequencyButton ) { - return; - } - frequencyButton.click(); - const submitButton = donationForm.querySelector( `button[type="submit"][name="donation_value_${ frequency }"][value="${ amount }"]` ); - if ( ! submitButton ) { - return; - } - submitButton.click(); - } else { - const amountInput = ( layout === 'untiered' ) ? - donationForm.querySelector( `input[name="donation_value_${ frequency }_untiered"]` ) : - donationForm.querySelector( `input[name="donation_value_${ frequency }"][value="${ amount }"]` ); - if ( frequencyInput && amountInput ) { - frequencyInput.checked = true; - if ( layout === 'untiered' ) { - amountInput.value = amount; - } else if ( amount === 'other' ) { - amountInput.click(); - const otherInput = donationForm.querySelector( `input[name="donation_value_${ frequency }_other"]` ); - if ( otherInput && other ) { - otherInput.value = other; - } - } else { - amountInput.checked = true; - } - form = donationForm; - } - } - } ); - if ( form ) { - triggerCheckout( form ); - } - } - - /** - * Handle checkout button form triggers. - * - * @param {number} productId The product ID. - * @param {number|null} variationId Optional. The variation ID. - */ - const triggerCheckoutButtonForm = ( productId, variationId = null ) => { - let form; - if ( variationId && variationId !== productId ) { - const variationModals = document.querySelectorAll( `.${ VARIATON_MODAL_CLASS_PREFIX }` ); - const variationModal = [ ...variationModals ].find( - modal => modal.dataset.productId === productId - ); - if ( variationModal ) { - const forms = variationModal.querySelectorAll( `form[target="${ IFRAME_NAME }"]` ); - forms.forEach( variationForm => { - const productData = JSON.parse( variationForm.dataset.product ); - if ( productData?.variation_id === Number( variationId ) ) { - form = variationForm; - } - } ); - } - } else { - const checkoutButtons = document.querySelectorAll( '.wp-block-newspack-blocks-checkout-button' ); - checkoutButtons.forEach( button => { - const checkoutButtonForm = button.querySelector( 'form' ); - if ( ! checkoutButtonForm ) { - return; - } - const productData = JSON.parse( checkoutButtonForm.dataset.product ); - if ( productData?.product_id === productId ) { - form = checkoutButtonForm; - } - } ); - } - if ( form ) { - triggerCheckout( form ); - } - } - /** * Handle modal checkout url param triggers. */ const handleModalCheckoutUrlParams = () => { const urlParams = new URLSearchParams( window.location.search ); - if ( ! urlParams.has( MODAL_CHECKOUT_ID ) ) { + if ( ! urlParams.has( 'checkout' ) ) { return; } - - const type = urlParams.get( 'type' ); - if ( type === 'donate' ) { - const layout = urlParams.get( 'layout' ); - const frequency = urlParams.get( 'frequency' ); - const amount = urlParams.get( 'amount' ); - const other = urlParams.get( 'other' ); - if ( layout && frequency && amount ) { - triggerDonationForm( layout, frequency, amount, other ); - } - } - if ( type === 'checkout_button' ) { - const productId = urlParams.get( 'product_id' ); - const variationId = urlParams.get( 'variation_id' ); - if ( productId ) { - triggerCheckoutButtonForm( productId, variationId ); - } + const url = window.newspackReaderActivation?.getPendingCheckout?.(); + if ( url ) { + const form = generateCheckoutPageForm( url ); + triggerCheckout( form ); } // Remove the URL param to prevent re-triggering. window.history.replaceState( null, null, window.location.pathname ); From 047850d54742225a5001687ad51fed0d37d55701 Mon Sep 17 00:00:00 2001 From: Miguel Peixe Date: Fri, 11 Oct 2024 15:44:19 -0300 Subject: [PATCH 12/20] fix: update method names --- src/modal-checkout/modal.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/modal-checkout/modal.js b/src/modal-checkout/modal.js index 477a21cf9..528d0cd4d 100644 --- a/src/modal-checkout/modal.js +++ b/src/modal-checkout/modal.js @@ -498,7 +498,7 @@ domReady( () => { window.history.back(); } } - window?.newspackReaderActivation?.resetCheckoutData?.(); + window?.newspackReaderActivation?.setPendingCheckout?.(); }; if ( window?.newspackReaderActivation?.openNewslettersSignupModal ) { @@ -516,7 +516,7 @@ domReady( () => { setModalTitle( newspackBlocksModal.labels.checkout_modal_title ); } } else { - window?.newspackReaderActivation?.resetCheckoutData?.(); + window?.newspackReaderActivation?.setPendingCheckout?.(); // Track a dismissal event (modal has been manually closed without completing the checkout). manageDismissed(); From 893a9c41576a432465ded2fc9ac84186690b0ee0 Mon Sep 17 00:00:00 2001 From: Miguel Peixe Date: Fri, 11 Oct 2024 16:11:57 -0300 Subject: [PATCH 13/20] feat: filter allowed assets --- includes/class-modal-checkout.php | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/includes/class-modal-checkout.php b/includes/class-modal-checkout.php index 22a79a413..419b1e5a9 100644 --- a/includes/class-modal-checkout.php +++ b/includes/class-modal-checkout.php @@ -655,6 +655,13 @@ public static function dequeue_scripts() { '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 ) { From 35b6d59c5146f3588fe0b09316582274715809b6 Mon Sep 17 00:00:00 2001 From: Miguel Peixe Date: Fri, 11 Oct 2024 16:20:02 -0300 Subject: [PATCH 14/20] fix: restore `is_newspack_donate` and use guard clause --- includes/class-modal-checkout.php | 215 +++++++++++++++--------------- 1 file changed, 109 insertions(+), 106 deletions(-) diff --git a/includes/class-modal-checkout.php b/includes/class-modal-checkout.php index 419b1e5a9..070f2ebd7 100644 --- a/includes/class-modal-checkout.php +++ b/includes/class-modal-checkout.php @@ -160,6 +160,16 @@ public static function process_checkout_request() { } $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 ); + + if ( ! $is_newspack_checkout && ! $is_newspack_donate ) { + return; + } + + // Flag the checkout as a registration for both newspack checkout and donate flows. + if ( $is_checkout_registration ) { + \WC()->session->set( self::CHECKOUT_REGISTRATION_FLAG, true ); + } if ( ! $is_newspack_checkout ) { return; @@ -172,135 +182,128 @@ public static function process_checkout_request() { $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 ); - // Flag the checkout as a registration. - if ( $is_checkout_registration ) { - \WC()->session->set( self::CHECKOUT_REGISTRATION_FLAG, true ); + if ( ! $product_id ) { + return; } - if ( $is_newspack_checkout ) { - if ( ! $product_id ) { - return; - } + if ( $variation_id ) { + $product_id = $variation_id; + } - 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 ); + } + 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 ); - } + // 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 ); + $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( $checkout_url ); - exit; - } + if ( defined( 'DOING_AJAX' ) ) { + echo wp_json_encode( [ 'url' => $checkout_url ] ); + exit; + } else { + // Redirect to checkout. + \wp_safe_redirect( $checkout_url ); + exit; } } From 6a68ca67f9f85edb2d4fab374fa75673a353b698 Mon Sep 17 00:00:00 2001 From: Miguel Peixe Date: Fri, 11 Oct 2024 16:29:18 -0300 Subject: [PATCH 15/20] fix: variable position --- includes/class-modal-checkout.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/includes/class-modal-checkout.php b/includes/class-modal-checkout.php index 070f2ebd7..201ea68d6 100644 --- a/includes/class-modal-checkout.php +++ b/includes/class-modal-checkout.php @@ -167,6 +167,7 @@ public static function process_checkout_request() { } // Flag the checkout as a registration for both newspack checkout and donate flows. + $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 ); } @@ -180,7 +181,6 @@ public static function process_checkout_request() { $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 ( ! $product_id ) { return; From 50f0e686f1719411ec5d82946fe21077010c6e29 Mon Sep 17 00:00:00 2001 From: Miguel Peixe Date: Fri, 11 Oct 2024 17:01:13 -0300 Subject: [PATCH 16/20] fix: isolate checkout registration flag --- includes/class-modal-checkout.php | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/includes/class-modal-checkout.php b/includes/class-modal-checkout.php index 201ea68d6..fafe370ac 100644 --- a/includes/class-modal-checkout.php +++ b/includes/class-modal-checkout.php @@ -57,6 +57,7 @@ public static function init() { 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' ] ); @@ -151,6 +152,18 @@ 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. */ @@ -160,17 +173,6 @@ public static function process_checkout_request() { } $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 ); - - if ( ! $is_newspack_checkout && ! $is_newspack_donate ) { - return; - } - - // Flag the checkout as a registration for both newspack checkout and donate flows. - $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 ); - } if ( ! $is_newspack_checkout ) { return; From 68c787ce91f4846e43ff4201901a3ab4191047cd Mon Sep 17 00:00:00 2001 From: Miguel Peixe Date: Fri, 11 Oct 2024 17:04:25 -0300 Subject: [PATCH 17/20] chore: remove extra line --- includes/class-modal-checkout.php | 1 - 1 file changed, 1 deletion(-) diff --git a/includes/class-modal-checkout.php b/includes/class-modal-checkout.php index fafe370ac..39e924613 100644 --- a/includes/class-modal-checkout.php +++ b/includes/class-modal-checkout.php @@ -163,7 +163,6 @@ public static function set_checkout_registration_flag() { } } - /** * Process checkout request for modal. */ From af02c11a8930c67821253d47afabadebd64d1910 Mon Sep 17 00:00:00 2001 From: Miguel Peixe Date: Fri, 11 Oct 2024 17:11:41 -0300 Subject: [PATCH 18/20] fix: use url query param for checkout registration flag --- src/modal-checkout/modal.js | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/modal-checkout/modal.js b/src/modal-checkout/modal.js index 528d0cd4d..b044c12bd 100644 --- a/src/modal-checkout/modal.js +++ b/src/modal-checkout/modal.js @@ -374,13 +374,11 @@ domReady( () => { title: newspackBlocksModal.labels.auth_modal_title, callback: ( message, authData ) => { cartReq.then( url => { - const checkoutForm = generateCheckoutPageForm( url ); - // Signal checkout registration. + // If registered, append the registration flag query param to the url. if ( authData?.registered ) { - checkoutForm.appendChild( - createHiddenInput( newspackBlocksModal.checkout_registration_flag, '1' ) - ); + url += `&${ newspackBlocksModal.checkout_registration_flag }=1`; } + const checkoutForm = generateCheckoutPageForm( url ); triggerCheckout( checkoutForm ); } ) .catch( error => { From bffe9297a6a96ead84dbffb094cde3a73e0f12ff Mon Sep 17 00:00:00 2001 From: Miguel Peixe Date: Fri, 11 Oct 2024 17:17:57 -0300 Subject: [PATCH 19/20] chore: improve docblock --- src/modal-checkout/modal.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/modal-checkout/modal.js b/src/modal-checkout/modal.js index b044c12bd..667c67e82 100644 --- a/src/modal-checkout/modal.js +++ b/src/modal-checkout/modal.js @@ -130,7 +130,11 @@ domReady( () => { iframe.addEventListener( 'load', handleIframeReady ); /** - * Generate cart. + * Generate cart via ajax. + * + * This strategy, used for anonymous users, addresses an edge case in which + * the session for a newly registered reader fails to carry the cart over to + * the checkout. * * @return {Promise} The promise that resolves with the checkout URL. */ From 21f542cc6cea76f7bdee1f881100bd7f23424564 Mon Sep 17 00:00:00 2001 From: Miguel Peixe Date: Fri, 11 Oct 2024 17:38:48 -0300 Subject: [PATCH 20/20] fix: restore checkout url params functionality --- src/modal-checkout/modal.js | 118 ++++++++++++++++++++++++++++++++++-- 1 file changed, 114 insertions(+), 4 deletions(-) diff --git a/src/modal-checkout/modal.js b/src/modal-checkout/modal.js index 667c67e82..cc058ae9c 100644 --- a/src/modal-checkout/modal.js +++ b/src/modal-checkout/modal.js @@ -653,6 +653,99 @@ domReady( () => { form.requestSubmit( form.querySelector( 'button[type="submit"]' ) ); } + /** + * Handle donation form triggers. + * + * @param {string} layout The donation layout. + * @param {string} frequency The donation frequency. + * @param {string} amount The donation amount. + * @param {string|null} other Optional. The custom amount when other is selected. + */ + const triggerDonationForm = ( layout, frequency, amount, other = null ) => { + let form; + document.querySelectorAll( '.wpbnbd.wpbnbd--platform-wc form' ) + .forEach( donationForm => { + const frequencyInput = donationForm.querySelector( `input[name="donation_frequency"][value="${ frequency }"]` ); + if ( ! frequencyInput ) { + return; + } + if ( layout === 'tiered' ) { + const frequencyButton = document.querySelector( `button[data-frequency-slug="${ frequency }"]` ); + if ( ! frequencyButton ) { + return; + } + frequencyButton.click(); + const submitButton = donationForm.querySelector( `button[type="submit"][name="donation_value_${ frequency }"][value="${ amount }"]` ); + if ( ! submitButton ) { + return; + } + submitButton.click(); + } else { + const amountInput = ( layout === 'untiered' ) ? + donationForm.querySelector( `input[name="donation_value_${ frequency }_untiered"]` ) : + donationForm.querySelector( `input[name="donation_value_${ frequency }"][value="${ amount }"]` ); + if ( frequencyInput && amountInput ) { + frequencyInput.checked = true; + if ( layout === 'untiered' ) { + amountInput.value = amount; + } else if ( amount === 'other' ) { + amountInput.click(); + const otherInput = donationForm.querySelector( `input[name="donation_value_${ frequency }_other"]` ); + if ( otherInput && other ) { + otherInput.value = other; + } + } else { + amountInput.checked = true; + } + form = donationForm; + } + } + } ); + if ( form ) { + triggerCheckout( form ); + } + } + + /** + * Handle checkout button form triggers. + * + * @param {number} productId The product ID. + * @param {number|null} variationId Optional. The variation ID. + */ + const triggerCheckoutButtonForm = ( productId, variationId = null ) => { + let form; + if ( variationId && variationId !== productId ) { + const variationModals = document.querySelectorAll( `.${ VARIATON_MODAL_CLASS_PREFIX }` ); + const variationModal = [ ...variationModals ].find( + modal => modal.dataset.productId === productId + ); + if ( variationModal ) { + const forms = variationModal.querySelectorAll( `form[target="${ IFRAME_NAME }"]` ); + forms.forEach( variationForm => { + const productData = JSON.parse( variationForm.dataset.product ); + if ( productData?.variation_id === Number( variationId ) ) { + form = variationForm; + } + } ); + } + } else { + const checkoutButtons = document.querySelectorAll( '.wp-block-newspack-blocks-checkout-button' ); + checkoutButtons.forEach( button => { + const checkoutButtonForm = button.querySelector( 'form' ); + if ( ! checkoutButtonForm ) { + return; + } + const productData = JSON.parse( checkoutButtonForm.dataset.product ); + if ( productData?.product_id === productId ) { + form = checkoutButtonForm; + } + } ); + } + if ( form ) { + triggerCheckout( form ); + } + } + /** * Handle modal checkout url param triggers. */ @@ -661,10 +754,27 @@ domReady( () => { if ( ! urlParams.has( 'checkout' ) ) { return; } - const url = window.newspackReaderActivation?.getPendingCheckout?.(); - if ( url ) { - const form = generateCheckoutPageForm( url ); - triggerCheckout( form ); + const type = urlParams.get( 'type' ); + if ( type === 'donate' ) { + const layout = urlParams.get( 'layout' ); + const frequency = urlParams.get( 'frequency' ); + const amount = urlParams.get( 'amount' ); + const other = urlParams.get( 'other' ); + if ( layout && frequency && amount ) { + triggerDonationForm( layout, frequency, amount, other ); + } + } else if ( type === 'checkout_button' ) { + const productId = urlParams.get( 'product_id' ); + const variationId = urlParams.get( 'variation_id' ); + if ( productId ) { + triggerCheckoutButtonForm( productId, variationId ); + } + } else { + const url = window.newspackReaderActivation?.getPendingCheckout?.(); + if ( url ) { + const form = generateCheckoutPageForm( url ); + triggerCheckout( form ); + } } // Remove the URL param to prevent re-triggering. window.history.replaceState( null, null, window.location.pathname );