'', 'inserter' => false, 'content' => '

' . esc_html__( 'Checkout', 'woocommerce' ) . '

', ) ); } /** * Get the editor script handle for this block type. * * @param string $key Data to get, or default to everything. * @return array|string; */ protected function get_block_type_editor_script( $key = null ) { $script = [ 'handle' => 'wc-' . $this->block_name . '-block', 'path' => $this->asset_api->get_block_asset_build_path( $this->block_name ), 'dependencies' => [ 'wc-blocks' ], ]; return $key ? $script[ $key ] : $script; } /** * Get the frontend script handle for this block type. * * @see $this->register_block_type() * @param string $key Data to get, or default to everything. * @return array|string */ protected function get_block_type_script( $key = null ) { $dependencies = []; // Load password strength meter script asynchronously if needed. if ( ! is_user_logged_in() && 'no' === get_option( 'woocommerce_registration_generate_password' ) ) { $dependencies[] = 'zxcvbn-async'; } $script = [ 'handle' => 'wc-' . $this->block_name . '-block-frontend', 'path' => $this->asset_api->get_block_asset_build_path( $this->block_name . '-frontend' ), 'dependencies' => $dependencies, ]; return $key ? $script[ $key ] : $script; } /** * Get the frontend style handle for this block type. * * @return string[] */ protected function get_block_type_style() { return array_merge( parent::get_block_type_style(), [ 'wc-blocks-packages-style' ] ); } /** * Enqueue frontend assets for this block, just in time for rendering. * * @param array $attributes Any attributes that currently are available from the block. * @param string $content The block content. * @param WP_Block $block The block object. */ protected function enqueue_assets( array $attributes, $content, $block ) { /** * Fires before checkout block scripts are enqueued. * * @since 4.6.0 */ do_action( 'woocommerce_blocks_enqueue_checkout_block_scripts_before' ); parent::enqueue_assets( $attributes, $content, $block ); /** * Fires after checkout block scripts are enqueued. * * @since 4.6.0 */ do_action( 'woocommerce_blocks_enqueue_checkout_block_scripts_after' ); } /** * Append frontend scripts when rendering the block. * * @param array $attributes Block attributes. * @param string $content Block content. * @param WP_Block $block Block instance. * @return string Rendered block type output. */ protected function render( $attributes, $content, $block ) { if ( $this->is_checkout_endpoint() ) { // Note: Currently the block only takes care of the main checkout form -- if an endpoint is set, refer to the // legacy shortcode instead and do not render block. return wc_current_theme_is_fse_theme() ? do_shortcode( '[woocommerce_checkout]' ) : '[woocommerce_checkout]'; } // Dequeue the core scripts when rendering this block. add_action( 'wp_enqueue_scripts', array( $this, 'dequeue_woocommerce_core_scripts' ), 20 ); /** * We need to check if $content has any templates from prior iterations of the block, in order to update to the latest iteration. * We test the iteration version by searching for new blocks brought in by it. * The blocks used for testing should be always available in the block (not removable by the user). * Checkout i1's content was returning an empty div, with no data-block-name attribute */ $regex_for_empty_block = '/
<\/div>/mi'; $has_i1_template = preg_match( $regex_for_empty_block, $content ); if ( $has_i1_template ) { // This fallback needs to match the default templates defined in our Blocks. $inner_blocks_html = '
' . ( isset( $attributes['showOrderNotes'] ) && false === $attributes['showOrderNotes'] ? '' : '
' ) . ( isset( $attributes['showPolicyLinks'] ) && false === $attributes['showPolicyLinks'] ? '' : '
' ) . '
'; $content = str_replace( '
', $inner_blocks_html . '', $content ); } /** * Checkout i3 added inner blocks for Order summary. * We need to add them to Checkout i2 templates. * The order needs to match the order in which these blocks were registered. */ $order_summary_with_inner_blocks = '$0
'; // Order summary subtotal block was added in i3, so we search for it to see if we have a Checkout i2 template. $regex_for_order_summary_subtotal = '/]*?>/mi'; $regex_for_order_summary = '/]*?>/mi'; $has_i2_template = ! preg_match( $regex_for_order_summary_subtotal, $content ); if ( $has_i2_template ) { $content = preg_replace( $regex_for_order_summary, $order_summary_with_inner_blocks, $content ); } /** * Add the Local Pickup toggle to checkouts missing this forced template. */ $local_pickup_inner_blocks = '
' . PHP_EOL . PHP_EOL . '
' . PHP_EOL . PHP_EOL . '$0'; $has_local_pickup_regex = '/]*?>/mi'; $has_local_pickup = preg_match( $has_local_pickup_regex, $content ); if ( ! $has_local_pickup ) { $shipping_address_block_regex = '/]*?><\/div>/mi'; $content = preg_replace( $shipping_address_block_regex, $local_pickup_inner_blocks, $content ); } /** * Add the Additional Information block to checkouts missing it. */ $additional_information_inner_blocks = '$0' . PHP_EOL . PHP_EOL . '
' . PHP_EOL . PHP_EOL; $has_additional_information_regex = '/]*?>/mi'; $has_additional_information_block = preg_match( $has_additional_information_regex, $content ); if ( ! $has_additional_information_block ) { $payment_block_regex = '/]*?><\/div>/mi'; $content = preg_replace( $payment_block_regex, $additional_information_inner_blocks, $content ); } return $content; } /** * Check if we're viewing a checkout page endpoint, rather than the main checkout page itself. * * @return boolean */ protected function is_checkout_endpoint() { return is_wc_endpoint_url( 'order-pay' ) || is_wc_endpoint_url( 'order-received' ); } /** * Update the local pickup title in WooCommerce Settings when the checkout page containing a Checkout block is saved. * * @param int $post_id The post ID. * @param \WP_Post $post The post object. * @return void */ public function update_local_pickup_title( $post_id, $post ) { // This is not a proper save action, maybe an autosave, so don't continue. if ( empty( $post->post_status ) || 'inherit' === $post->post_status ) { return; } // Check if we are editing the checkout page and that it contains a Checkout block. // Cast to string for Checkout page ID comparison because get_option can return it as a string, so better to compare both values as strings. if ( ! empty( $post->post_type ) && 'wp_template' !== $post->post_type && ( false === has_block( 'woocommerce/checkout', $post ) || (string) get_option( 'woocommerce_checkout_page_id' ) !== (string) $post_id ) ) { return; } if ( ( ! empty( $post->post_type ) && ! empty( $post->post_name ) && 'page-checkout' !== $post->post_name && 'wp_template' === $post->post_type ) || false === has_block( 'woocommerce/checkout', $post ) ) { return; } $pickup_location_settings = LocalPickupUtils::get_local_pickup_settings( 'edit' ); if ( ! isset( $pickup_location_settings['title'] ) ) { return; } if ( empty( $post->post_content ) ) { return; } $post_blocks = parse_blocks( $post->post_content ); $title = $this->find_local_pickup_text_in_checkout_block( $post_blocks ); if ( $title ) { $pickup_location_settings['title'] = $title; update_option( 'woocommerce_pickup_location_settings', $pickup_location_settings ); } } /** * Recurse through the blocks to find the shipping methods block, then get the value of the localPickupText attribute from it. * * @param array $blocks The block(s) to search for the local pickup text. * @return null|string The local pickup text if found, otherwise void. */ private function find_local_pickup_text_in_checkout_block( $blocks ) { if ( ! is_array( $blocks ) ) { return null; } foreach ( $blocks as $block ) { if ( ! empty( $block['blockName'] ) && 'woocommerce/checkout-shipping-method-block' === $block['blockName'] ) { if ( ! empty( $block['attrs']['localPickupText'] ) ) { return $block['attrs']['localPickupText']; } } if ( ! empty( $block['innerBlocks'] ) ) { $answer = $this->find_local_pickup_text_in_checkout_block( $block['innerBlocks'] ); if ( $answer ) { return $answer; } } } } /** * Extra data passed through from server to client for block. * * @param array $attributes Any attributes that currently are available from the block. * Note, this will be empty in the editor context when the block is * not in the post content on editor load. */ protected function enqueue_data( array $attributes = [] ) { parent::enqueue_data( $attributes ); $country_data = CartCheckoutUtils::get_country_data(); $address_formats = WC()->countries->get_address_formats(); // Move the address format into the 'countryData' setting. // We need to skip 'default' because that's not a valid country. foreach ( $address_formats as $country_code => $format ) { if ( 'default' === $country_code ) { continue; } $country_data[ $country_code ]['format'] = $format; } $this->asset_data_registry->add( 'countryData', $country_data ); $this->asset_data_registry->add( 'defaultAddressFormat', $address_formats['default'] ); $this->asset_data_registry->add( 'baseLocation', wc_get_base_location() ); $this->asset_data_registry->add( 'checkoutAllowsGuest', false === filter_var( wc()->checkout()->is_registration_required(), FILTER_VALIDATE_BOOLEAN ) ); $this->asset_data_registry->add( 'checkoutAllowsSignup', filter_var( wc()->checkout()->is_registration_enabled(), FILTER_VALIDATE_BOOLEAN ) ); $this->asset_data_registry->add( 'checkoutShowLoginReminder', filter_var( get_option( 'woocommerce_enable_checkout_login_reminder' ), FILTER_VALIDATE_BOOLEAN ) ); $this->asset_data_registry->add( 'displayCartPricesIncludingTax', 'incl' === get_option( 'woocommerce_tax_display_cart' ) ); $this->asset_data_registry->add( 'displayItemizedTaxes', 'itemized' === get_option( 'woocommerce_tax_total_display' ) ); $this->asset_data_registry->add( 'forcedBillingAddress', 'billing_only' === get_option( 'woocommerce_ship_to_destination' ) ); $this->asset_data_registry->add( 'generatePassword', filter_var( get_option( 'woocommerce_registration_generate_password' ), FILTER_VALIDATE_BOOLEAN ) ); $this->asset_data_registry->add( 'taxesEnabled', wc_tax_enabled() ); $this->asset_data_registry->add( 'couponsEnabled', wc_coupons_enabled() ); $this->asset_data_registry->add( 'shippingEnabled', wc_shipping_enabled() ); $this->asset_data_registry->add( 'hasDarkEditorStyleSupport', current_theme_supports( 'dark-editor-style' ) ); $this->asset_data_registry->register_page_id( isset( $attributes['cartPageId'] ) ? $attributes['cartPageId'] : 0 ); $this->asset_data_registry->add( 'isBlockTheme', wc_current_theme_is_fse_theme() ); $pickup_location_settings = LocalPickupUtils::get_local_pickup_settings(); $local_pickup_method_ids = LocalPickupUtils::get_local_pickup_method_ids(); $this->asset_data_registry->add( 'localPickupEnabled', $pickup_location_settings['enabled'] ); $this->asset_data_registry->add( 'localPickupText', $pickup_location_settings['title'] ); $this->asset_data_registry->add( 'collectableMethodIds', $local_pickup_method_ids ); $is_block_editor = $this->is_block_editor(); // Hydrate the following data depending on admin or frontend context. if ( $is_block_editor && ! $this->asset_data_registry->exists( 'shippingMethodsExist' ) ) { $methods_exist = wc_get_shipping_method_count( false, true ) > 0; $this->asset_data_registry->add( 'shippingMethodsExist', $methods_exist ); } if ( $is_block_editor && ! $this->asset_data_registry->exists( 'globalShippingMethods' ) ) { $shipping_methods = WC()->shipping()->get_shipping_methods(); $formatted_shipping_methods = array_reduce( $shipping_methods, function ( $acc, $method ) use ( $local_pickup_method_ids ) { if ( in_array( $method->id, $local_pickup_method_ids, true ) ) { return $acc; } if ( $method->supports( 'settings' ) ) { $acc[] = [ 'id' => $method->id, 'title' => $method->method_title, 'description' => $method->method_description, ]; } return $acc; }, [] ); $this->asset_data_registry->add( 'globalShippingMethods', $formatted_shipping_methods ); } if ( $is_block_editor && ! $this->asset_data_registry->exists( 'activeShippingZones' ) && class_exists( '\WC_Shipping_Zones' ) ) { $this->asset_data_registry->add( 'activeShippingZones', CartCheckoutUtils::get_shipping_zones() ); } if ( $is_block_editor && ! $this->asset_data_registry->exists( 'globalPaymentMethods' ) ) { // These are used to show options in the sidebar. We want to get the full list of enabled payment methods, // not just the ones that are available for the current cart (which may not exist yet). $payment_methods = $this->get_enabled_payment_gateways(); $formatted_payment_methods = array_reduce( $payment_methods, function ( $acc, $method ) { $acc[] = [ 'id' => $method->id, 'title' => $method->method_title, 'description' => $method->method_description, ]; return $acc; }, [] ); $this->asset_data_registry->add( 'globalPaymentMethods', $formatted_payment_methods ); } if ( $is_block_editor && ! $this->asset_data_registry->exists( 'incompatibleExtensions' ) ) { if ( ! class_exists( '\Automattic\WooCommerce\Utilities\FeaturesUtil' ) || ! function_exists( 'get_plugins' ) ) { return; } $declared_extensions = \Automattic\WooCommerce\Utilities\FeaturesUtil::get_compatible_plugins_for_feature( 'cart_checkout_blocks' ); $all_plugins = \get_plugins(); // Note that `get_compatible_plugins_for_feature` calls `get_plugins` internally, so this is already in cache. $incompatible_extensions = array_reduce( $declared_extensions['incompatible'], function ( $acc, $item ) use ( $all_plugins ) { $plugin = $all_plugins[ $item ] ?? null; $plugin_id = $plugin['TextDomain'] ?? dirname( $item, 2 ); $plugin_name = $plugin['Name'] ?? $plugin_id; $acc[] = [ 'id' => $plugin_id, 'title' => $plugin_name, ]; return $acc; }, [] ); $this->asset_data_registry->add( 'incompatibleExtensions', $incompatible_extensions ); } if ( ! is_admin() && ! WC()->is_rest_api_request() ) { $this->asset_data_registry->hydrate_api_request( '/wc/store/v1/cart' ); $this->asset_data_registry->hydrate_data_from_api_request( 'checkoutData', '/wc/store/v1/checkout' ); $this->hydrate_customer_payment_methods(); } /** * Fires after checkout block data is registered. * * @since 2.6.0 */ do_action( 'woocommerce_blocks_checkout_enqueue_data' ); } /** * Get payment methods that are enabled in settings. * * @return array */ protected function get_enabled_payment_gateways() { $payment_gateways = WC()->payment_gateways->payment_gateways(); return array_filter( $payment_gateways, function ( $payment_gateway ) { return 'yes' === $payment_gateway->enabled; } ); } /** * Are we currently on the admin block editor screen? */ protected function is_block_editor() { if ( ! is_admin() || ! function_exists( 'get_current_screen' ) ) { return false; } $screen = get_current_screen(); return $screen && $screen->is_block_editor(); } /** * Get saved customer payment methods for use in checkout. */ protected function hydrate_customer_payment_methods() { if ( ! is_user_logged_in() || $this->asset_data_registry->exists( 'customerPaymentMethods' ) ) { return; } add_filter( 'woocommerce_payment_methods_list_item', [ $this, 'include_token_id_with_payment_methods' ], 10, 2 ); $payment_gateways = $this->get_enabled_payment_gateways(); $payment_methods = wc_get_customer_saved_methods_list( get_current_user_id() ); // Filter out payment methods that are not enabled. foreach ( $payment_methods as $payment_method_group => $saved_payment_methods ) { $payment_methods[ $payment_method_group ] = array_values( array_filter( $saved_payment_methods, function ( $saved_payment_method ) use ( $payment_gateways ) { return in_array( $saved_payment_method['method']['gateway'], array_keys( $payment_gateways ), true ); } ) ); } $this->asset_data_registry->add( 'customerPaymentMethods', $payment_methods ); remove_filter( 'woocommerce_payment_methods_list_item', [ $this, 'include_token_id_with_payment_methods' ], 10, 2 ); } /** * Callback for woocommerce_payment_methods_list_item filter to add token id * to the generated list. * * @param array $list_item The current list item for the saved payment method. * @param \WC_Token $token The token for the current list item. * * @return array The list item with the token id added. */ public static function include_token_id_with_payment_methods( $list_item, $token ) { $list_item['tokenId'] = $token->get_id(); $brand = ! empty( $list_item['method']['brand'] ) ? strtolower( $list_item['method']['brand'] ) : ''; // phpcs:ignore WordPress.WP.I18n.TextDomainMismatch -- need to match on translated value from core. if ( ! empty( $brand ) && esc_html__( 'Credit card', 'woocommerce' ) !== $brand ) { $list_item['method']['brand'] = wc_get_credit_card_type_label( $brand ); } return $list_item; } /** * Register script and style assets for the block type before it is registered. * * This registers the scripts; it does not enqueue them. */ protected function register_block_type_assets() { parent::register_block_type_assets(); $chunks = $this->get_chunks_paths( $this->chunks_folder ); $vendor_chunks = $this->get_chunks_paths( 'vendors--checkout-blocks' ); $shared_chunks = [ 'cart-blocks/cart-express-payment--checkout-blocks/express-payment-frontend' ]; $this->register_chunk_translations( array_merge( $chunks, $vendor_chunks, $shared_chunks ) ); } /** * Get list of Checkout block & its inner-block types. * * @return array; */ public static function get_checkout_block_types() { return [ 'Checkout', 'CheckoutActionsBlock', 'CheckoutAdditionalInformationBlock', 'CheckoutBillingAddressBlock', 'CheckoutContactInformationBlock', 'CheckoutExpressPaymentBlock', 'CheckoutFieldsBlock', 'CheckoutOrderNoteBlock', 'CheckoutOrderSummaryBlock', 'CheckoutOrderSummaryCartItemsBlock', 'CheckoutOrderSummaryCouponFormBlock', 'CheckoutOrderSummaryDiscountBlock', 'CheckoutOrderSummaryFeeBlock', 'CheckoutOrderSummaryShippingBlock', 'CheckoutOrderSummarySubtotalBlock', 'CheckoutOrderSummaryTaxesBlock', 'CheckoutOrderSummaryTotalsBlock', 'CheckoutPaymentBlock', 'CheckoutShippingAddressBlock', 'CheckoutShippingMethodsBlock', 'CheckoutShippingMethodBlock', 'CheckoutPickupOptionsBlock', 'CheckoutTermsBlock', 'CheckoutTotalsBlock', ]; } }