Skip to content

Instantly share code, notes, and snippets.

@damiencarbery
Created July 8, 2026 19:26
Show Gist options
  • Select an option

  • Save damiencarbery/4184f4be434f3c5cd39d0a7028fb5bb0 to your computer and use it in GitHub Desktop.

Select an option

Save damiencarbery/4184f4be434f3c5cd39d0a7028fb5bb0 to your computer and use it in GitHub Desktop.
Checkout block
<?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