diff --git a/modules/ppcp-paylater-wc-blocks/resources/js/CartPayLaterMessagesBlock/block.json b/modules/ppcp-paylater-wc-blocks/resources/js/CartPayLaterMessagesBlock/block.json index e6bdedea0..ff11fe681 100644 --- a/modules/ppcp-paylater-wc-blocks/resources/js/CartPayLaterMessagesBlock/block.json +++ b/modules/ppcp-paylater-wc-blocks/resources/js/CartPayLaterMessagesBlock/block.json @@ -18,7 +18,6 @@ "lock": { "type": "object", "default": { - "remove": true, "move": false } } diff --git a/modules/ppcp-paylater-wc-blocks/resources/js/CartPayLaterMessagesBlock/cart-paylater-block-inserter.js b/modules/ppcp-paylater-wc-blocks/resources/js/CartPayLaterMessagesBlock/cart-paylater-block-inserter.js new file mode 100644 index 000000000..f1581ee65 --- /dev/null +++ b/modules/ppcp-paylater-wc-blocks/resources/js/CartPayLaterMessagesBlock/cart-paylater-block-inserter.js @@ -0,0 +1,121 @@ +(function(wp) { + const { createBlock } = wp.blocks; + const { select, dispatch, subscribe } = wp.data; + const getBlocks = () => select('core/block-editor').getBlocks() || []; + + // Store the initial list of block client IDs + let blockList = getBlocks().map(block => block.clientId); + + /** + * Subscribes to changes in the block editor, specifically checking for the presence of 'woocommerce/cart'. + */ + subscribe(() => { + const currentBlocks = getBlocks(); + + currentBlocks.forEach(block => { + if (block.name === 'woocommerce/cart') { + ensurePayLaterBlockExists(block); + } + }); + }); + + /** + * Ensures the 'woocommerce-paypal-payments/cart-paylater-messages' block exists inside the 'woocommerce/cart' block. + * @param {Object} cartBlock - The cart block instance. + */ + function ensurePayLaterBlockExists(cartBlock) { + const payLaterBlock = findBlockByName(cartBlock.innerBlocks, 'woocommerce-paypal-payments/cart-paylater-messages'); + if (!payLaterBlock) { + waitForBlock('woocommerce/cart-totals-block', 'woocommerce-paypal-payments/cart-paylater-messages', 'woocommerce/cart-order-summary-block'); + } + } + + /** + * Waits for a specific block to appear using async/await pattern before executing the insertBlockAfter function. + * @param {string} targetBlockName - Name of the block to wait for. + * @param {string} newBlockName - Name of the new block to insert after the target. + * @param {string} anchorBlockName - Name of the anchor block to determine position. + * @param {number} attempts - The number of attempts made to find the target block. + */ + async function waitForBlock(targetBlockName, newBlockName, anchorBlockName = '', attempts = 0) { + const targetBlock = findBlockByName(getBlocks(), targetBlockName); + if (targetBlock) { + await delay(1000); // We need this to ensure the block is fully rendered + insertBlockAfter(targetBlockName, newBlockName, anchorBlockName); + } else if (attempts < 10) { // Poll up to 10 times + await delay(1000); // Wait 1 second before retrying + await waitForBlock(targetBlockName, newBlockName, anchorBlockName, attempts + 1); + } else { + console.log('Failed to find target block after several attempts.'); + } + } + + /** + * Delays execution by a given number of milliseconds. + * @param {number} ms - Milliseconds to delay. + * @return {Promise} A promise that resolves after the delay. + */ + function delay(ms) { + return new Promise(resolve => setTimeout(resolve, ms)); + } + + /** + * Inserts a block after a specified block if it doesn't already exist. + * @param {string} targetBlockName - Name of the block to find. + * @param {string} newBlockName - Name of the new block to insert. + * @param {string} anchorBlockName - Name of the anchor block to determine position. + */ + function insertBlockAfter(targetBlockName, newBlockName, anchorBlockName = '') { + const targetBlock = findBlockByName(getBlocks(), targetBlockName); + if (!targetBlock) { + // Target block not found + return; + } + + const parentBlock = select('core/block-editor').getBlock(targetBlock.clientId); + if (parentBlock.innerBlocks.some(block => block.name === newBlockName)) { + // The block is already inserted next to the target block + return; + } + + let offset = 0; + if (anchorBlockName !== '') { + // Find the anchor block and calculate the offset + const anchorIndex = parentBlock.innerBlocks.findIndex(block => block.name === anchorBlockName); + offset = parentBlock.innerBlocks.length - (anchorIndex + 1); + } + + const newBlock = createBlock(newBlockName); + + // Insert the block at the correct position + dispatch('core/block-editor').insertBlock(newBlock, parentBlock.innerBlocks.length - offset, parentBlock.clientId); + + // Lock the block after it has been inserted + setTimeout(() => { + dispatch('core/block-editor').updateBlockAttributes(newBlock.clientId, { + lock: { remove: true } + }); + }, 1000); + } + + /** + * Recursively searches for a block by name among all blocks. + * @param {Array} blocks - The array of blocks to search. + * @param {string} blockName - The name of the block to find. + * @returns {Object|null} The found block, or null if not found. + */ + function findBlockByName(blocks, blockName) { + for (const block of blocks) { + if (block.name === blockName) { + return block; + } + if (block.innerBlocks.length > 0) { + const foundBlock = findBlockByName(block.innerBlocks, blockName); + if (foundBlock) { + return foundBlock; + } + } + } + return null; + } +})(window.wp); diff --git a/modules/ppcp-paylater-wc-blocks/resources/js/CartPayLaterMessagesBlock/cart-paylater-block.js b/modules/ppcp-paylater-wc-blocks/resources/js/CartPayLaterMessagesBlock/cart-paylater-block.js index cd77866ed..713365349 100644 --- a/modules/ppcp-paylater-wc-blocks/resources/js/CartPayLaterMessagesBlock/cart-paylater-block.js +++ b/modules/ppcp-paylater-wc-blocks/resources/js/CartPayLaterMessagesBlock/cart-paylater-block.js @@ -9,6 +9,8 @@ import { registerBlockType } from '@wordpress/blocks'; import Edit from './edit'; import metadata from './block.json'; +const { underTotalsPlacementEnabled } = window.PcpCartPayLaterBlock; + const paypalIcon = ( ); +metadata.attributes.lock.default.remove = !Boolean(underTotalsPlacementEnabled); + registerBlockType(metadata, { icon: paypalIcon, edit: Edit, diff --git a/modules/ppcp-paylater-wc-blocks/resources/js/CartPayLaterMessagesBlock/edit.js b/modules/ppcp-paylater-wc-blocks/resources/js/CartPayLaterMessagesBlock/edit.js index 4746aab23..58546c5f3 100644 --- a/modules/ppcp-paylater-wc-blocks/resources/js/CartPayLaterMessagesBlock/edit.js +++ b/modules/ppcp-paylater-wc-blocks/resources/js/CartPayLaterMessagesBlock/edit.js @@ -6,7 +6,7 @@ import { PayPalScriptProvider, PayPalMessages } from '@paypal/react-paypal-js'; import { useScriptParams } from '../../../../ppcp-paylater-block/resources/js/hooks/script-params'; export default function Edit({ attributes, clientId, setAttributes }) { - const { id, ppcpId } = attributes; + const { ppcpId } = attributes; const [loaded, setLoaded] = useState(false); diff --git a/modules/ppcp-paylater-wc-blocks/resources/js/CheckoutPayLaterMessagesBlock/edit.js b/modules/ppcp-paylater-wc-blocks/resources/js/CheckoutPayLaterMessagesBlock/edit.js index 0774baa12..3b081cb69 100644 --- a/modules/ppcp-paylater-wc-blocks/resources/js/CheckoutPayLaterMessagesBlock/edit.js +++ b/modules/ppcp-paylater-wc-blocks/resources/js/CheckoutPayLaterMessagesBlock/edit.js @@ -6,7 +6,7 @@ import { PayPalScriptProvider, PayPalMessages } from '@paypal/react-paypal-js'; import { useScriptParams } from '../../../../ppcp-paylater-block/resources/js/hooks/script-params'; export default function Edit({ attributes, clientId, setAttributes }) { - const { id, ppcpId } = attributes; + const { ppcpId } = attributes; const [loaded, setLoaded] = useState(false); diff --git a/modules/ppcp-paylater-wc-blocks/src/PayLaterWCBlocksModule.php b/modules/ppcp-paylater-wc-blocks/src/PayLaterWCBlocksModule.php index 300ced6a1..70e6afe88 100644 --- a/modules/ppcp-paylater-wc-blocks/src/PayLaterWCBlocksModule.php +++ b/modules/ppcp-paylater-wc-blocks/src/PayLaterWCBlocksModule.php @@ -59,6 +59,19 @@ class PayLaterWCBlocksModule implements ModuleInterface { return self::is_block_enabled( $settings_status, $location ); } + /** + * Returns whether the under cart totals placement is enabled. + * + * @return bool true if the under cart totals placement is enabled, otherwise false. + */ + public function is_under_cart_totals_placement_enabled() : bool { + return apply_filters( + // phpcs:ignore WordPress.NamingConventions.ValidHookName.UseUnderscores + 'woocommerce.feature-flags.woocommerce_paypal_payments.paylater_wc_blocks_cart_under_totals_enabled', + true + ); + } + /** * {@inheritDoc} */ @@ -103,16 +116,17 @@ class PayLaterWCBlocksModule implements ModuleInterface { $script_handle, 'PcpCartPayLaterBlock', array( - 'ajax' => array( + 'ajax' => array( 'cart_script_params' => array( 'endpoint' => \WC_AJAX::get_endpoint( CartScriptParamsEndpoint::ENDPOINT ), ), ), - 'config' => $config_factory->from_settings( $settings ), - 'settingsUrl' => admin_url( 'admin.php?page=wc-settings&tab=checkout§ion=ppcp-gateway' ), - 'vaultingEnabled' => $settings->has( 'vault_enabled' ) && $settings->get( 'vault_enabled' ), - 'placementEnabled' => self::is_placement_enabled( $c->get( 'wcgateway.settings.status' ), 'cart' ), - 'payLaterSettingsUrl' => admin_url( 'admin.php?page=wc-settings&tab=checkout§ion=ppcp-gateway&ppcp-tab=ppcp-pay-later' ), + 'config' => $config_factory->from_settings( $settings ), + 'settingsUrl' => admin_url( 'admin.php?page=wc-settings&tab=checkout§ion=ppcp-gateway' ), + 'vaultingEnabled' => $settings->has( 'vault_enabled' ) && $settings->get( 'vault_enabled' ), + 'placementEnabled' => self::is_placement_enabled( $c->get( 'wcgateway.settings.status' ), 'cart' ), + 'payLaterSettingsUrl' => admin_url( 'admin.php?page=wc-settings&tab=checkout§ion=ppcp-gateway&ppcp-tab=ppcp-pay-later' ), + 'underTotalsPlacementEnabled' => self::is_under_cart_totals_placement_enabled(), ) ); @@ -218,7 +232,8 @@ class PayLaterWCBlocksModule implements ModuleInterface { 'woocommerce-paypal-payments/cart-paylater-messages', 'ppcp-cart-paylater-messages', 'cart', - $c + $c, + self::is_under_cart_totals_placement_enabled() ); } return $block_content; @@ -245,6 +260,27 @@ class PayLaterWCBlocksModule implements ModuleInterface { 10, 1 ); + + // Since there's no regular way we can place the Pay Later messaging block under the cart totals block, we need a custom script. + if ( self::is_under_cart_totals_placement_enabled() ) { + add_action( + 'enqueue_block_editor_assets', + function () use ( $c, $settings ): void { + $handle = 'ppcp-checkout-paylater-block-editor-inserter'; + $path = $c->get( 'paylater-wc-blocks.url' ) . 'assets/js/cart-paylater-block-inserter.js'; + + wp_register_script( + $handle, + $path, + array( 'wp-blocks', 'wp-data', 'wp-element' ), + $c->get( 'ppcp.asset-version' ), + true + ); + + wp_enqueue_script( $handle ); + } + ); + } } /** diff --git a/modules/ppcp-paylater-wc-blocks/src/PayLaterWCBlocksUtils.php b/modules/ppcp-paylater-wc-blocks/src/PayLaterWCBlocksUtils.php index 298828cc3..8bb4df628 100644 --- a/modules/ppcp-paylater-wc-blocks/src/PayLaterWCBlocksUtils.php +++ b/modules/ppcp-paylater-wc-blocks/src/PayLaterWCBlocksUtils.php @@ -31,6 +31,24 @@ class PayLaterWCBlocksUtils { return $block_content; } + /** + * Inserts content after the closing div tag of a specific block. + * + * @param string $block_content The block content. + * @param string $content_to_insert The content to insert. + * @param string $reference_block The block markup to insert the content after. + * @return string The block content with the content inserted. + */ + public static function insert_before_opening_div( string $block_content, string $content_to_insert, string $reference_block ): string { + $reference_block_index = strpos( $block_content, $reference_block ); + + if ( false !== $reference_block_index ) { + return substr_replace( $block_content, $content_to_insert, $reference_block_index, 0 ); + } else { + return self::insert_before_last_div( $block_content, $content_to_insert ); + } + } + /** * Renders a PayLater message block and inserts it before the last closing div tag if the block id is not already present. * @@ -39,12 +57,19 @@ class PayLaterWCBlocksUtils { * @param string $ppcp_id ID for the PPCP component. * @param string $context Rendering context (cart or checkout). * @param mixed $container Dependency injection container. + * @param bool $is_under_cart_totals_placement_enabled Whether the block should be placed under the cart totals. * @return string Updated block content. */ - public static function render_and_insert_paylater_block( string $block_content, string $block_id, string $ppcp_id, string $context, $container ): string { - $paylater_message_block = self::render_paylater_block( $block_id, $ppcp_id, $context, $container ); + public static function render_and_insert_paylater_block( string $block_content, string $block_id, string $ppcp_id, string $context, $container, bool $is_under_cart_totals_placement_enabled = false ): string { + $paylater_message_block = self::render_paylater_block( $block_id, $ppcp_id, $context, $container ); + $cart_express_payment_block = '
'; + if ( false !== $paylater_message_block ) { - return self::insert_before_last_div( $block_content, $paylater_message_block ); + if ( $is_under_cart_totals_placement_enabled && 'cart' === $context ) { + return self::insert_before_opening_div( $block_content, $paylater_message_block, $cart_express_payment_block ); + } else { + return self::insert_before_last_div( $block_content, $paylater_message_block ); + } } return $block_content; } diff --git a/modules/ppcp-paylater-wc-blocks/webpack.config.js b/modules/ppcp-paylater-wc-blocks/webpack.config.js index c8326c939..4bf41ebfa 100644 --- a/modules/ppcp-paylater-wc-blocks/webpack.config.js +++ b/modules/ppcp-paylater-wc-blocks/webpack.config.js @@ -16,6 +16,13 @@ module.exports = { "CartPayLaterMessagesBlock", "cart-paylater-block.js" ), + "cart-paylater-block-inserter": path.resolve( + process.cwd(), + "resources", + "js", + "CartPayLaterMessagesBlock", + "cart-paylater-block-inserter.js" + ), "checkout-paylater-block": path.resolve( process.cwd(), "resources",