Created
July 8, 2026 19:26
-
-
Save damiencarbery/4184f4be434f3c5cd39d0a7028fb5bb0 to your computer and use it in GitHub Desktop.
Checkout block
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <?php | |
| /* | |
| Plugin Name: Sourdough and Beyond redesign | |
| Plugin URI: https://www.damiencarbery.com | |
| Description: Customisations and experiments for Sourdough and Beyond redesign. | |
| Author: Damien Carbery | |
| Version: 0.1.20260703 | |
| Requires Plugins: cmb2, woocommerce | |
| */ | |
| defined( 'ABSPATH' ) || exit; | |
| class SourdoughAndBeyond { | |
| // The single instance of the class. | |
| public static $instance = null; | |
| // The number of delivery dates to render. | |
| private $max_delivery_days; | |
| // Returns an instance of this class. | |
| public static function instance() { | |
| if ( null == self::$instance ) { | |
| self::$instance = new self; | |
| } | |
| return self::$instance; | |
| } | |
| // Initialize the plugin variables. | |
| public function __construct() { | |
| // Initialise the default number of delivery dates to render. | |
| $this->max_delivery_days = 4; // | |
| $this->init(); | |
| } | |
| // Set up WordPress specfic actions. | |
| public function init() { | |
| // Register the PHP-only Delivery Dates block. | |
| add_action( 'init', array( $this, 'register_delivery_dates_block' ) ); | |
| // Load the DateTimePicker CSS and JS on the checkout page. | |
| add_action( 'wp_enqueue_scripts', array( $this, 'datetimepicker_assets_on_checkout' ) ); | |
| // Enqueue the CSS to display the block output correctly in the editor. | |
| // ToDo: This does not appear to be working. | |
| add_action( 'enqueue_block_editor_assets', array( $this, 'enqueue_block_editor_css' ) ); | |
| // Make a shortcode version of the block available. | |
| add_shortcode( 'show_delivery_dates', array( $this, 'show_delivery_dates_checkboxes' ) ); | |
| // Add the "2. Add your favourites" text before the Easy Tabs buttons. | |
| add_filter( 'render_block', array( $this, 'add_text_before_easy_tabs_buttons' ), 10, 2 ); | |
| // Remove some 'Easy Tabs Block' styles to make it easier to add custom styles. | |
| add_action( 'wp_footer', array( $this, 'remove_etb_styles' ), 1 ); | |
| // Optional: Add the Delivery Dates checkboxes to the Shop page. | |
| add_action( 'woocommerce_before_shop_loop', array( $this, 'show_delivery_dates_checkboxes' ), 25 ); | |
| // Optional: Add the Delivery Dates checkboxes to the single product page. | |
| add_action( 'woocommerce_single_product_summary', array( $this, 'show_delivery_dates_checkboxes' ), 25 ); | |
| // Optional: Add delivery days to the single product page. | |
| add_action( 'woocommerce_single_product_summary', array( $this, 'template_single_display_days' ), 40 ); | |
| // Verify that the products in the cart can be delivered on the selected day. | |
| add_action( 'woocommerce_after_checkout_validation', array( $this, 'verify_cart_contents_and_delivery_day' ), 10, 2 ); | |
| // // This did not work because it does not have access to WC()->cart(). | |
| // add_filter( 'rest_authentication_errors', array( $this, 'rest_verify_cart_contents_and_delivery_day' ) ); | |
| } | |
| // Check whether the current page is a checkout page. | |
| // From: https://wordpress.org/plugins/delivery-date-time-slot-picker-for-woocommerce | |
| private function is_checkout_page() { | |
| if ( function_exists( 'is_checkout' ) && is_checkout() ) { | |
| return true; | |
| } | |
| if ( is_singular() ) { | |
| return has_block( 'woocommerce/checkout', get_the_ID() ); | |
| } | |
| return false; | |
| } | |
| public function register_delivery_dates_block() { | |
| wp_register_style( 'sab-delivery-dates', plugins_url( 'css/sab-delivery-dates.css', __FILE__ ), array() ); | |
| register_block_type( | |
| 'sab/delivery-dates', | |
| array( | |
| 'title' => 'S&B Delivery Dates', | |
| 'attributes' => array( | |
| 'dates_to_render' => array( | |
| 'type' => 'string', | |
| 'enum' => array( '1', '2', '3', '4', '5', '6', '7', '8' ), | |
| 'default' => $this->max_delivery_days, | |
| 'label' => 'Number of dates to show (recommend 4)', | |
| ), | |
| ), | |
| 'render_callback' => array( $this, 'delivery_dates_checkboxes_block' ), | |
| 'supports' => array( | |
| 'autoRegister' => true, | |
| ), | |
| ) | |
| ); | |
| } | |
| // Load the DateTimePicker CSS and JS on the checkout page. | |
| public function datetimepicker_assets_on_checkout() { | |
| if ( $this->is_checkout_page() ) { | |
| wp_enqueue_style( 'sourdough-delivery-dates', plugins_url( 'css/sab-delivery-dates.css', __FILE__ ), array() ); | |
| wp_enqueue_style( 'jquery-ui-pepper-grinder', 'https://ajax.googleapis.com/ajax/libs/jqueryui/1.14.1/themes/pepper-grinder/jquery-ui.css' ); | |
| wp_enqueue_script( 'jquery-ui-datepicker' ); | |
| // Configure and handle the jQuery DatePicker. | |
| wp_enqueue_script( 'sab-datepicker', plugins_url( 'js/sab-checkout-datepicker.js', __FILE__ ), array( 'jquery-ui-datepicker' ) ); | |
| // Make the list of excluded dates available to the script. | |
| $excluded_dates = get_option( 'delivery_dates' ); | |
| if ( array_key_exists( 'dates', $excluded_dates ) ) { | |
| $excluded_dates = $excluded_dates['dates']; | |
| } | |
| // Examine the cart and create a list of product IDs, names and categories. | |
| $products = array(); | |
| foreach ( WC()->cart->get_cart() as $cart_item_key => $cart_item ) { | |
| $product = $cart_item['data']; | |
| $product_cats = get_the_terms( $product->get_id(), 'product_cat' ); | |
| $product_cat_names = array(); | |
| foreach ( $product_cats as $cat ) { | |
| $product_cat_names[] = $cat->name; | |
| } | |
| $products[] = array( 'id' => $product->get_id(), 'name' => $product->get_name(), 'categories' => implode( ', ', $product_cat_names ) ); | |
| } | |
| //error_log( 'Cart items: ' . var_export( $products, true ) ); | |
| wp_add_inline_script( 'sab-datepicker', 'const blockedOutDates = ' . json_encode( $excluded_dates ) . ";\nconst cart_contents = " . json_encode( $products ) ); | |
| } | |
| } | |
| // Enqueue the CSS to display the block output correctly in the editor. | |
| // ToDo: This does not appear to be working. | |
| public function enqueue_block_editor_css() { | |
| wp_enqueue_style( 'sourdough-delivery-dates', plugins_url( 'css/sab-delivery-dates.css', __FILE__ ), array() ); | |
| } | |
| // Create a clickable list of future delivery dates. | |
| public function delivery_dates_checkboxes_block( $attributes ) { | |
| $valid_days = array( 4, 5, 6, 7 ); // 1 (for Monday) through 7 (for Sunday) | |
| // Change $max_delivery_days if block setting has a value. | |
| if ( isset( $attributes['dates_to_render'] ) && intval( $attributes['dates_to_render'] ) > 0 ) { | |
| $this->max_delivery_days = intval( $attributes['dates_to_render'] ); | |
| } | |
| $delivery_dates = array(); | |
| // 2 day lead time so start our list there. | |
| $delivery_date = DateTime::createFromFormat( 'U', (string) time() + ( 2 * DAY_IN_SECONDS ) ); | |
| // Get list of excluded dates (CMB2 code) and move the subarray up a level. | |
| $excluded_dates = get_option( 'delivery_dates' ); | |
| //error_log( 'excluded_dates:' . var_export( $excluded_dates, true ) ); | |
| if ( array_key_exists( 'dates', $excluded_dates ) ) { | |
| $excluded_dates = $excluded_dates['dates']; | |
| } | |
| //error_log( 'excluded_dates:' . var_export( $excluded_dates, true ) ); | |
| do { | |
| // Get delivery date in format that matches those in $excluded_dates. Date and month do not have leading zeros, to simplify working with Javascript Date object. | |
| $exclude_format = sprintf( '%s-%s-%s', $delivery_date->format( 'j' ), $delivery_date->format( 'n' ), $delivery_date->format( 'Y' ) ); | |
| // If it is a valid delivery day of the week then store it. | |
| if ( in_array( $delivery_date->format( 'N' ), $valid_days ) && !in_array( $exclude_format, $excluded_dates ) ) { | |
| // For the data-delivery-day-name attribute. | |
| $day_name = strtolower( $delivery_date->format( 'l' ) ); // monday, tuesday etc. | |
| // The <label for=""> attribute e.g. radio_delivery-monday-21. | |
| $label_for = sprintf( '%s-%d', $day_name, $delivery_date->format( 'd' ) ); // e.g. monday-21 | |
| // The <label> text is like 'Mon<br>June 22'. | |
| $abbrev_day_name = $delivery_date->format( 'D' ); // Mon, Tues etc. | |
| $month_date = $delivery_date->format( 'F j' ); // e.g. June 22 | |
| // ToDo: What if date or month are less than 10?? The JS will need to match the format. | |
| $stored_date = sprintf( '%s-%s-%s', $delivery_date->format( 'Y' ), $delivery_date->format( 'm' ), $delivery_date->format( 'd' ) ); | |
| $checked = ''; | |
| if ( ! count( $delivery_dates ) ) { | |
| $checked = 'checked="checked"'; | |
| } | |
| $delivery_dates[] = sprintf( '<input type="radio" name="delivery-date" id="radio_delivery-%s" %s>' . | |
| '<label data-delivery-day-name="%s" data-stored-date="%s" for="radio_delivery-%s" class="delivery-%s"><span class="day_name">%s</span><br>%s</label>', | |
| $label_for, $checked, $day_name, $stored_date, $label_for, $day_name, $abbrev_day_name, $month_date ); | |
| } | |
| // Move on one day. | |
| $delivery_date = $delivery_date->add( DateInterval::createFromDateString( '1 day' ) ); | |
| } while ( count( $delivery_dates ) < $this->max_delivery_days ); | |
| // Enqueue the CSS and JS instead of using has_block() or has_shortcode() first. | |
| //wp_enqueue_style( 'sab-delivery-dates', plugins_url( 'sab-delivery-dates.css', __FILE__ ) ); | |
| wp_enqueue_style( 'sab-delivery-dates' ); | |
| wp_enqueue_script( 'sab-delivery-dates', plugins_url( 'js/sab-delivery-dates.js', __FILE__ ) ); | |
| $output = '<div class="delivery_dates">' . implode( "\n", $delivery_dates ) . '</div>'; | |
| return $output; | |
| } | |
| // Shortcode function to display the Delivery Dates checkboxes block markup. | |
| public function show_delivery_dates_checkboxes() { | |
| echo '<div class="delivery_dates_shortcode"><p><strong>Choose your delivery day</strong></p>' . $this->delivery_dates_checkboxes_block( array() ) . '<p><small>ToDo: Disable "Add to cart" button if not selling item on chosen day.</small></p></div>'; | |
| } | |
| // Add the "2. Add your favourites" text before the Easy Tabs buttons. | |
| public function add_text_before_easy_tabs_buttons( $block_content, $block ) { | |
| if ( isset( $block['blockName'] ) && 'easy-tabs-block/tab-buttons' === $block['blockName'] ) { | |
| //error_log( var_export( $block_content, true ) ); | |
| return str_replace( 'role="tablist">', 'role="tablist"><p class="prompt">2. Add your favourites</p>', $block_content ); | |
| } | |
| return $block_content; | |
| } | |
| // Remove some 'Easy Tabs Block' styles to make it easier to add custom styles. | |
| public function remove_etb_styles() { | |
| wp_dequeue_style('easy-tabs-block-tab-button-style'); | |
| //wp_dequeue_style('easy-tabs-block-tab-buttons-style'); | |
| //wp_dequeue_style('easy-tabs-block-tab-content-style'); | |
| //wp_dequeue_style('easy-tabs-block-tab-contents-style'); | |
| //wp_dequeue_style('easy-tabs-block-tabs-style'); // Cannot remove as it creates tab functionality. | |
| //wp_dequeue_style('easy-tabs-block-tabs-style'); | |
| } | |
| // Add delivery days to the single product page. | |
| public function template_single_display_days() { | |
| global $product; | |
| $product_cats = get_the_terms( $product->get_id(), 'product_cat' ); | |
| // Map the category slugs to day of week number (for later sorting). | |
| // The values must match the order in the $dow_to_name array. | |
| $cat_to_dow = array( 'delivery-monday' => 0, 'delivery-tuesday' => 1, 'delivery-wednesday' => 2, 'delivery-thursday' => 3, | |
| 'delivery-friday' => 4, 'delivery-saturday' => 5, 'delivery-sunday' => 6 ); | |
| $dow_to_name = array( 'Mondays', 'Tuesdays', 'Wednesdays', 'Thursdays', 'Fridays', 'Saturdays', 'Sundays' ); | |
| $delivery_dows = array(); | |
| // Get the day of week number for each category. | |
| foreach ( $product_cats as $cat ) { | |
| if ( isset( $cat_to_dow[ $cat->slug ] ) ) { | |
| $delivery_dows[] = $cat_to_dow[ $cat->slug ]; | |
| } | |
| } | |
| if ( ! empty( $delivery_dows ) ) { | |
| if ( count( $delivery_dows ) < 7) { | |
| $delivery_days = array(); | |
| sort( $delivery_dows ); // Sort the array values. | |
| // And convert the numeric values back to day names. | |
| foreach ( $delivery_dows as $dow ) { | |
| $delivery_days[] = '<strong>' . $dow_to_name[ $dow ] . '</strong>'; | |
| } | |
| echo '<p class="deliver_on_days">This item can be delivered on ', implode( ', ', $delivery_days ), '.</p>'; | |
| } | |
| } | |
| else { | |
| if ( is_user_logged_in() ) { | |
| echo '<p class="deliver_on_days">ADMIN NOTE: Delivery day categories are not set for this product.</p>'; | |
| } | |
| } | |
| } | |
| // Verify that the products in the cart can be delivered on the selected day. | |
| public function verify_cart_contents_and_delivery_day( $fields, $errors ) { | |
| //error_log( '$fields:' . var_export( $fields, true ) ); | |
| if ( preg_match( '/\\S/', $fields[ 'billing_first_name' ] ) || preg_match( '/\\S/', $fields[ 'billing_last_name' ] ) ) { | |
| $errors->add( 'validation', 'Your first or last name contains a letter.' ); | |
| } | |
| } | |
| /* public function rest_verify_cart_contents_and_delivery_day( $result ) { | |
| // Skip if this is not a POST request. | |
| if ( isset( $_SERVER['REQUEST_METHOD'] ) && $_SERVER['REQUEST_METHOD'] !== 'POST' ) { | |
| // Always return the result or an error, never a boolean. This ensures other checks aren't thrown away like rate limiting or authentication. | |
| return $result; | |
| } | |
| // Skip if this is not the checkout endpoint. | |
| if ( ! preg_match( '#/wc/store(?:/v\d+)?/checkout#', $GLOBALS['wp']->query_vars['rest_route'] ) ) { | |
| return $result; | |
| } | |
| error_log( 'REST route: ' . $GLOBALS['wp']->query_vars['rest_route'] ); | |
| // get request body | |
| $request_body = json_decode( \WP_REST_Server::get_raw_data(), true ); | |
| error_log( '$request_body: ' . var_export( $request_body, true ) ); | |
| $products = array(); | |
| foreach ( WC()->cart->get_cart() as $cart_item_key => $cart_item ) { | |
| $product = $cart_item['data']; | |
| $products[] = $product->get_name() . ': ' . implode( ', ', $product->get_category_ids() ); | |
| } | |
| error_log( 'Cart items: ' . var_export( $products, true ) ); | |
| return new WP_Error( 'verify_cart_failed', 'Verify cart failed.' ); | |
| if ( isset( $request_body['payment_method'] ) ) { | |
| $chosen_payment_method = sanitize_text_field( $request_body['payment_method'] ); | |
| // Provide ability to short circuit the check to allow express payments or hosted checkouts to bypass the check. | |
| $selected_payment_methods = apply_filters( 'plugin_payment_methods_to_skip', array('woocommerce_payments' ) ); | |
| if( is_array( $selected_payment_methods ) ) { | |
| if ( in_array( $chosen_payment_method, $selected_payment_methods, true ) ) { | |
| return $result; | |
| } | |
| } | |
| } | |
| $extensions = $request_body['extensions']; | |
| if ( empty( $extensions ) || ! isset( $extensions['plugin-namespace-turnstile'] ) ) { | |
| return new WP_Error( 'challenge_failed', 'Captcha challenge failed' ); | |
| } | |
| $token = sanitize_text_field( $extensions['plugin-namespace-turnstile']['token'] ); | |
| // Note: The function `my_token_check_function` would be implemented in your plugin to handle token validation. | |
| $check = my_token_check_function( $token ); | |
| $success = $check['success']; | |
| if( $success !== true ) { | |
| return new WP_Error( 'challenge_failed', 'Captcha challenge failed' ); | |
| } | |
| return $result; | |
| }*/ | |
| } | |
| $SourdoughAndBeyond = new SourdoughAndBeyond(); | |
| // This code is for a text field in the Additional order information area. | |
| add_action( 'woocommerce_init', 'cbt_delivery_date_order_field' ); | |
| function cbt_delivery_date_order_field() { | |
| woocommerce_register_additional_checkout_field( | |
| array( | |
| // id, label and location are required. | |
| 'id' => 'sab/delivery-date', | |
| 'label' => 'Delivery Date', // Label when field is required. | |
| 'location' => 'order', // order, address, contact. For address it will show in Shipping *and* Billing sections. | |
| 'required' => true, | |
| 'placeholder' => /*'What day would you like us to deliver your order?*/'Deliveries are between 7:30am and 10:30am Thursday to Sunday', | |
| 'type' => 'text', | |
| 'attributes' => array( | |
| 'autocomplete' => 'off' | |
| // To help users on mobile devices you can turn on autocomplete assistance and autocapitalisation. | |
| //'autocomplete' => 'given-name', // email, given-name, family-name, country, organization, address-line1, address-line2, address-level1, address-level2, postal-code, tel. https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Attributes/autocomplete#values | |
| //'autocapitalize' => 'on', // off, on, none, sentences, words, characters. This only applies on mobile devices. | |
| // The aria attributes can be used for accessibility. | |
| //'aria-describedby' => 'some-element', // Accessibility setting. | |
| //'aria-label' => 'custom aria label', // Accessibility setting. | |
| // The custom data can be added for JavaScript use. | |
| //'data-custom' => 'custom data', // Custom attribute for custom JS use. | |
| ), | |
| //'validate_callback' => 'cbt_second_email_validation_callback', | |
| //'sanitize_callback' => 'cbt_second_email_sanitization_callback', // Optional function to santize field value. | |
| ), | |
| ); | |
| } | |
| // Validate the Second email field to ensure it is an email address and return a message on error. | |
| function cbt_second_email_validation_callback( $value, $field ) { | |
| if ( ! is_email( $value ) ) { | |
| // Use different label if field is not required. | |
| $field_name = $field['required'] ? $field['label'] : $field['optionalLabel']; | |
| return new WP_Error( 'invalid_second_email', sprintf( 'The "%s" field must be an email address.', $field_name ) ); | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment