Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: v2 widget in modal checkout #3645

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 14 additions & 12 deletions includes/class-recaptcha.php
Original file line number Diff line number Diff line change
Expand Up @@ -344,18 +344,6 @@ public static function can_use_captcha( $version = null ) {
return false;
}

// Only use v2 if we are in modal checkout context.
// TODO: Remove this check once we have a way to enable v2 for non-modal checkouts.
if (
( 'v2' === $version || 'v2_invisible' === $settings['version'] ) &&
(
! method_exists( 'Newspack_Blocks\Modal_Checkout', 'is_modal_checkout' ) ||
! \Newspack_Blocks\Modal_Checkout::is_modal_checkout()
)
) {
return false;
}

if ( empty( self::get_site_key() ) || empty( self::get_site_secret() ) ) {
return false;
}
Expand Down Expand Up @@ -510,6 +498,20 @@ function refreshToken() {
public static function verify_recaptcha_on_checkout() {
$url = \home_url( \add_query_arg( null, null ) );
$should_verify_captcha = apply_filters( 'newspack_recaptcha_verify_captcha', self::can_use_captcha(), $url );
$version = self::get_setting( 'version' );

// Only use v2 if we are in modal checkout context.
// TODO: Remove this check once we have a way to enable v2 for non-modal checkouts.
if (
( 'v2' === $version || 'v2_invisible' === $version ) &&
(
! method_exists( 'Newspack_Blocks\Modal_Checkout', 'is_modal_checkout' ) ||
! \Newspack_Blocks\Modal_Checkout::is_modal_checkout()
)
) {
$should_verify_captcha = false;
}

if ( ! $should_verify_captcha ) {
return;
}
Expand Down
237 changes: 56 additions & 181 deletions src/other-scripts/recaptcha/index.js
Original file line number Diff line number Diff line change
@@ -1,29 +1,16 @@
/* globals jQuery, grecaptcha, newspack_recaptcha_data */

import {
addErrorMessage,
addHiddenV3Field,
destroyV3Field,
domReady,
getIntersectionObserver,
refreshV2Widget,
removeErrorMessages
} from './utils';
import './style.scss';

/**
* Specify a function to execute when the DOM is fully loaded.
*
* @see https://github.com/WordPress/gutenberg/blob/trunk/packages/dom-ready/
*
* @param {Function} callback A function to execute after the DOM is ready.
* @return {void}
*/
function domReady( callback ) {
if ( typeof document === 'undefined' ) {
return;
}
if (
document.readyState === 'complete' || // DOMContentLoaded + Images/Styles/etc loaded, so we call directly.
document.readyState === 'interactive' // DOMContentLoaded fires at this point, so we call directly.
) {
return void callback();
}
// DOMContentLoaded has not fired yet, delay callback until then.
document.addEventListener( 'DOMContentLoaded', callback );
}

window.newspack_grecaptcha = window.newspack_grecaptcha || {
destroy: destroyV3Field,
render,
Expand All @@ -35,87 +22,6 @@ const isV3 = 'v3' === newspack_recaptcha_data.version;
const siteKey = newspack_recaptcha_data.site_key;
const isInvisible = 'v2_invisible' === newspack_recaptcha_data.version;

/**
* Destroy hidden reCAPTCHA v3 token fields to avoid unnecessary reCAPTCHA checks.
*/
function destroyV3Field( forms = [] ) {
if ( isV3 ) {
const formsToHandle = forms.length
? forms
: [ ...document.querySelectorAll( 'form[data-newspack-recaptcha]' ) ];

formsToHandle.forEach( form => {
removeHiddenV3Field( form );
} );
}
}

/**
* Refresh the reCAPTCHA v3 token for the given form and action.
*
* @param {HTMLElement} field The hidden input field storing the token for a form.
* @param {string} action The action name to pass to reCAPTCHA.
*
* @return {Promise<void>|void} A promise that resolves when the token is refreshed.
*/
function refreshV3Token( field, action = 'submit' ) {
if ( field ) {
// Get a token to pass to the server. See https://developers.google.com/recaptcha/docs/v3 for API reference.
return grecaptcha.execute( siteKey, { action } ).then( token => {
field.value = token;
} );
}
}

/**
* Append a hidden reCAPTCHA v3 token field to the given form.
*
* @param {HTMLElement} form The form element.
*/
function addHiddenV3Field( form ) {
let field = form.querySelector( 'input[name="g-recaptcha-response"]' );
if ( ! field ) {
field = document.createElement( 'input' );
field.type = 'hidden';
field.name = 'g-recaptcha-response';
form.appendChild( field );

const action = form.getAttribute( 'data-newspack-recaptcha' ) || 'submit';
refreshV3Token( field, action );
setInterval( () => refreshV3Token( field, action ), 30000 ); // Refresh token every 30 seconds.

// Refresh reCAPTCHAs on Woo checkout update and error.
if ( jQuery ) {
jQuery( document ).on( 'updated_checkout', () => refreshV3Token( field, action ) );
jQuery( document.body ).on( 'checkout_error', () => refreshV3Token( field, action ) );
}
}
}

/**
* Remove the hidden reCAPTCHA v3 token field from the given form.
*
* @param {HTMLElement} form The form element.
*/
function removeHiddenV3Field( form ) {
const field = form.querySelector( 'input[name="g-recaptcha-response"]' );
if ( field ) {
field.parentElement.removeChild( field );
}
}

/**
* Refresh the reCAPTCHA v2 widget attached to the given element.
*
* @param {HTMLElement} el Element with the reCAPTCHA widget to refresh.
*/
function refreshV2Widget( el ) {
const widgetId = parseInt( el.getAttribute( 'data-recaptcha-widget-id' ) );
if ( ! isNaN( widgetId ) ) {
grecaptcha.reset( widgetId );
}
}

/**
* Render reCAPTCHA v2 widget on the given form.
*
Expand All @@ -139,14 +45,18 @@ function renderV2Widget( form, onSuccess = null, onError = null ) {
if ( button.hasAttribute( 'data-skip-recaptcha' ) ) {
return;
}
// Refresh widget if it already exists.
if ( button.hasAttribute( 'data-recaptcha-widget-id' ) ) {
refreshV2Widget( button );
return;
}
// Callback when reCAPTCHA passes validation or skip flag is present.
const successCallback = () => {
onSuccess?.()
const successCallback = token => {
onSuccess?.();
// Ensure the token gets submitted with the form submission.
let hiddenField = form.querySelector( '[name="g-recaptcha-response"]' );
if ( ! hiddenField ) {
hiddenField = document.createElement( 'input' );
hiddenField.type = 'hidden';
hiddenField.name = 'g-recaptcha-response';
form.appendChild( hiddenField );
}
hiddenField.value = token;
form.requestSubmit( button );
refreshV2Widget( button );
};
Expand All @@ -166,76 +76,51 @@ function renderV2Widget( form, onSuccess = null, onError = null ) {
}
}
}
// Attach widget to form events.
const attachListeners = () => {
getIntersectionObserver( () => renderV2Widget( form, onSuccess, onError ) ).observe( form, { attributes: true } );
button.addEventListener( 'click', e => {
e.preventDefault();
e.stopImmediatePropagation();
// Empty error messages if present.
removeErrorMessages( form );
// Skip reCAPTCHA verification if the button has a data-skip-recaptcha attribute.
if ( button.hasAttribute( 'data-skip-recaptcha' ) ) {
successCallback();
} else {
grecaptcha.execute( widgetId ).then( () => {
// If we are in an iframe scroll to top.
if ( window?.location !== window?.parent?.location ) {
document.body.scrollIntoView( { behavior: 'smooth' } );
}
} );
}
} );
}
// Refresh reCAPTCHA widgets on Woo checkout update and error.
if ( jQuery ) {
jQuery( document ).on( 'updated_checkout', () => attachListeners );
jQuery( document.body ).on( 'checkout_error', () => attachListeners );
}
// Refresh widget if it already exists.
if ( button.hasAttribute( 'data-recaptcha-widget-id' ) ) {
refreshV2Widget( button );
return;
}
const container = document.createElement( 'div' );
container.classList.add( 'grecaptcha-container' );
button.parentElement.append( container );
document.body.append( container );
const widgetId = grecaptcha.render( container, {
...options,
callback: successCallback,
'error-callback': errorCallback,
'expired-callback': errorCallback,
} );
button.setAttribute( 'data-recaptcha-widget-id', widgetId );

// Refresh reCAPTCHA widgets on Woo checkout update and error.
if ( jQuery ) {
jQuery( document ).on( 'updated_checkout', () => renderV2Widget( form, onSuccess, onError ) );
jQuery( document.body ).on( 'checkout_error', () => renderV2Widget( form, onSuccess, onError ) );
}

button.addEventListener( 'click', e => {
e.preventDefault();
e.stopImmediatePropagation();
// Empty error messages if present.
removeErrorMessages( form );
// Skip reCAPTCHA verification if the button has a data-skip-recaptcha attribute.
if ( button.hasAttribute( 'data-skip-recaptcha' ) ) {
successCallback();
} else {
grecaptcha.execute( widgetId ).then( () => {
// If we are in an iframe scroll to top.
if ( window?.location !== window?.parent?.location ) {
document.body.scrollIntoView( { behavior: 'smooth' } );
}
} );
}
} );
attachListeners();
} );
}

/**
* Append a generic error message above the given form.
*
* @param {HTMLElement} form The form element.
* @param {string} message The error message to display.
*/
function addErrorMessage( form, message ) {
const errorText = document.createElement( 'p' );
errorText.textContent = message;
const container = document.createElement( 'div' );
container.classList.add( 'newspack-recaptcha-error' );
container.appendChild( errorText );
// Newsletters block errors render below the form.
if ( form.parentElement.classList.contains( 'newspack-newsletters-subscribe' ) ) {
form.append( container );
} else {
container.classList.add( 'newspack-ui__notice', 'newspack-ui__notice--error' );
form.insertBefore( container, form.firstChild );
}
}

/**
* Remove generic error messages from form if present.
*
* @param {HTMLElement} form The form element.
*/
function removeErrorMessages( form ) {
const errors = form.querySelectorAll( '.newspack-recaptcha-error' );
for ( const error of errors ) {
error.parentElement.removeChild( error );
}
}

/**
* Render reCAPTCHA elements.
*
Expand All @@ -254,25 +139,15 @@ function render( forms = [], onSuccess = null, onError = null ) {
: [ ...document.querySelectorAll( 'form[data-newspack-recaptcha]' ) ];

formsToHandle.forEach( form => {
if ( ! form.hasAttribute( 'data-recaptcha-rendered' ) ) {
form.addEventListener( 'focusin', () => {
if ( isV2 ) {
renderV2Widget( form, onSuccess, onError );
}
if ( isV3 ) {
addHiddenV3Field( form );
}
} );
form.setAttribute( 'data-recaptcha-rendered', 'true' );
} else {
// Call render methods to trigger refresh.
const renderForm = () => {
if ( isV2 ) {
renderV2Widget( form, onSuccess, onError );
}
if ( isV3 ) {
addHiddenV3Field( form );
}
}
};
getIntersectionObserver( renderForm ).observe( form, { attributes: true } );
} );
}

Expand Down
Loading
Loading