Cart PayPal Pay Later messaging block: Change the default insert position to under the totals

This commit is contained in:
Daniel Dudzic 2024-04-25 01:42:23 +02:00
parent d1b4d7721e
commit 287a4a2819
No known key found for this signature in database
GPG key ID: 31B40D33E3465483
8 changed files with 205 additions and 13 deletions

View file

@ -18,7 +18,6 @@
"lock": {
"type": "object",
"default": {
"remove": true,
"move": false
}
}

View file

@ -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);

View file

@ -9,6 +9,8 @@ import { registerBlockType } from '@wordpress/blocks';
import Edit from './edit';
import metadata from './block.json';
const { underTotalsPlacementEnabled } = window.PcpCartPayLaterBlock;
const paypalIcon = (
<svg width="584.798" height="720" viewBox="0 0 154.728 190.5">
<g transform="translate(898.192 276.071)">
@ -31,6 +33,8 @@ const paypalIcon = (
</svg>
);
metadata.attributes.lock.default.remove = !Boolean(underTotalsPlacementEnabled);
registerBlockType(metadata, {
icon: paypalIcon,
edit: Edit,

View file

@ -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);

View file

@ -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);

View file

@ -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&section=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&section=ppcp-gateway&ppcp-tab=ppcp-pay-later' ),
'config' => $config_factory->from_settings( $settings ),
'settingsUrl' => admin_url( 'admin.php?page=wc-settings&tab=checkout&section=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&section=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 );
}
);
}
}
/**

View file

@ -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 = '<div data-block-name="woocommerce/cart-express-payment-block" class="wp-block-woocommerce-cart-express-payment-block"></div>';
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;
}

View file

@ -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",