Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Select an option

  • Save xlplugins/c52ec052cfccd70e7b768ae06b526ed0 to your computer and use it in GitHub Desktop.

Select an option

Save xlplugins/c52ec052cfccd70e7b768ae06b526ed0 to your computer and use it in GitHub Desktop.
Happy Head - Move Turnstile widget above Order Now button.
/**
* Happy Head - Move Turnstile widget above Order Now button.
*
* WFACP re-renders .woocommerce-checkout-payment on every update_checkout AJAX call.
* ECFT outputs a bare .cf-turnstile div; Cloudflare only auto-renders on first page load,
* so we explicitly call turnstile.render() after each checkout fragment refresh.
*/
class Happy_Head_ECFT_Checkout_Position {
public function __construct() {
add_action( 'wfacp_after_checkout_page_found', [ $this, 'register_placement' ] );
add_action( 'wfacp_before_process_checkout_template_loader', [ $this, 'register_placement' ] );
add_action( 'wp', [ $this, 'remove_default_placement' ], 15 );
add_action( 'wp_footer', [ $this, 'enqueue_rerender_script' ], 999 );
add_action( 'wp_footer', [ $this, 'enqueue_styles' ], 999 );
add_filter( 'woocommerce_update_order_review_fragments', function ( $fragments ) {
unset( $fragments['.woocommerce-checkout-payment'] );
return $fragments;
} );
}
public function register_placement() {
if ( ! $this->is_enabled() ) {
return;
}
if ( has_action( 'wfacp_woocommerce_review_order_before_submit', [ $this, 'render_widget' ] ) ) {
return;
}
add_action( 'wfacp_woocommerce_review_order_before_submit', [ $this, 'render_widget' ], 10 );
}
public function render_widget() {
// Use ECFT's configured validation message — same text the server sends via wc_add_notice().
$error_msg = ecft_get_option( 'ecft_validation_error_message' );
echo '<div class="ecft-inline-error" style="display:none;" role="alert">'
. '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="flex-shrink:0;margin-top:1px">'
. '<path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3Z"/>'
. '<line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/>'
. '</svg>'
. '<span style="margin:0;">' . esc_html( $error_msg ) . '</span>'
. '</div>';
ecft_render_turnstile_wrapper();
}
public function remove_default_placement() {
if ( ! $this->is_enabled() ) {
return;
}
remove_action( 'woocommerce_checkout_after_customer_details', 'ecft_render_turnstile_wrapper' );
}
private function is_enabled() {
return function_exists( 'ecft_render_turnstile_wrapper' )
&& function_exists( 'ecft_get_option' )
&& 'yes' === ecft_get_option( 'ecft_enable_woo_checkout' );
}
public function enqueue_styles() {
if ( ! is_checkout() || ! $this->is_enabled() ) {
return;
}
echo '<style>
.ecft-turnstile, .cf-turnstile { margin: 16px 0 18px !important; text-align: left !important; }
.ecft-turnstile > div, .ecft-turnstile iframe,
.cf-turnstile > div, .cf-turnstile iframe { margin-left: 0 !important; margin-right: auto !important; }
.ecft-inline-error {
display: none;
align-items: flex-start;
gap: 8px;
background: #fff5f5;
border: 1px solid #f87171;
border-radius: 4px;
padding: 10px 14px;
margin-bottom: 10px;
color: #dc2626;
font-size: 14px;
line-height: 1.4;
}
</style>';
}
public function enqueue_rerender_script() {
if ( ! is_checkout() || ! $this->is_enabled() || is_user_logged_in() ) {
return;
}
?>
<script>
(function ($) {
function happyHeadEcftRerenderTurnstile() {
if (typeof turnstile === 'undefined') {
return;
}
document.querySelectorAll('#payment .ecft-turnstile.cf-turnstile').forEach(function (el) {
if (el.querySelector('iframe') || el.querySelector('input[name="cf-turnstile-response"]')) {
return;
}
turnstile.render(el, {
sitekey: el.getAttribute('data-sitekey'),
theme: el.getAttribute('data-theme') || 'auto',
language: el.getAttribute('data-language') || 'auto',
size: el.getAttribute('data-size') || 'normal',
appearance: el.getAttribute('data-appearance') || 'always'
});
});
}
// Pre-submit: show inline error when place_order clicked with no token yet.
document.addEventListener('click', function (e) {
var btn = e.target.closest
? e.target.closest('#place_order, button[name="woocommerce_checkout_place_order"]')
: null;
if (!btn) { return; }
var tokenInput = document.querySelector('[name="cf-turnstile-response"]');
if (!tokenInput || tokenInput.value) { return; }
e.preventDefault();
e.stopImmediatePropagation();
var err = document.querySelector('.ecft-inline-error');
if (err) {
err.style.display = 'flex';
err.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
}
}, true);
// Post-submit: WC fires checkout_error with the notices HTML after a failed AJAX checkout.
// Find ECFT's notice in that HTML, show it inline, and remove it from the top notice area.
$(document.body).on('checkout_error', function (e, noticesHtml) {
var err = document.querySelector('.ecft-inline-error');
var msgEl = err ? err.querySelector('span') : null;
if (!err || !msgEl) { return; }
var ecftMsg = msgEl.textContent.trim();
// Parse the notices HTML WC returned and look for ECFT's message.
var tmp = document.createElement('div');
tmp.innerHTML = noticesHtml || '';
var items = tmp.querySelectorAll('li');
var matched = Array.prototype.slice.call(items).some(function (li) {
return li.textContent.trim() === ecftMsg;
});
if (!matched) { return; }
// Show inline.
err.style.display = 'flex';
err.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
// checkout_error fires AFTER WC/FunnelKit has already rendered the notices into the DOM.
// FunnelKit's wfacp_submit_error() prepends into .woocommerce-noticegroup-checkout (lowercase).
// WC's own submit_error() uses .woocommerce-NoticeGroup-checkout (capitalized).
// Scan all notice containers immediately — no timeout needed.
document.querySelectorAll([
'.woocommerce-noticegroup-checkout li',
'.woocommerce-NoticeGroup-checkout li',
'.woocommerce-notices-wrapper li',
'.wfacp-notices-wrapper li',
'.woocommerce-error li'
].join(', ')).forEach(function (li) {
if (li.textContent.trim() === ecftMsg) {
var ul = li.parentElement;
li.remove();
if (ul && ul.children.length === 0) { ul.remove(); }
}
});
});
// Hide inline error once Cloudflare fills the token (watches widget DOM changes).
function watchToken() {
var widget = document.querySelector('.ecft-turnstile, .cf-turnstile');
if (!widget) { return; }
new MutationObserver(function () {
var tokenInput = widget.querySelector('[name="cf-turnstile-response"]');
var err = document.querySelector('.ecft-inline-error');
if (tokenInput && tokenInput.value && err) {
err.style.display = 'none';
}
}).observe(widget, { subtree: true, childList: true, attributes: true });
}
$(function () {
happyHeadEcftRerenderTurnstile();
watchToken();
});
$(document.body).on('updated_checkout wfacp_updated_checkout', happyHeadEcftRerenderTurnstile);
})(jQuery);
</script>
<?php
}
}
new Happy_Head_ECFT_Checkout_Position();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment