Skip to content

Commit

Permalink
fix(recaptcha): render on focus
Browse files Browse the repository at this point in the history
  • Loading branch information
chickenn00dle committed Dec 16, 2024
1 parent cd8c9b5 commit 9d3a932
Showing 1 changed file with 102 additions and 107 deletions.
209 changes: 102 additions & 107 deletions src/other-scripts/recaptcha/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ function domReady( callback ) {
}

window.newspack_grecaptcha = window.newspack_grecaptcha || {
destroy,
destroy: destroyV3Field,
render,
version: newspack_recaptcha_data.version,
};
Expand All @@ -38,14 +38,14 @@ const isInvisible = 'v2_invisible' === newspack_recaptcha_data.version;
/**
* Destroy hidden reCAPTCHA v3 token fields to avoid unnecessary reCAPTCHA checks.
*/
function destroy( forms = [] ) {
function destroyV3Field( forms = [] ) {
if ( isV3 ) {
const formsToHandle = forms.length
? forms
: [ ...document.querySelectorAll( 'form[data-newspack-recaptcha]' ) ];

formsToHandle.forEach( form => {
removeHiddenField( form );
removeHiddenV3Field( form );
} );
}
}
Expand All @@ -58,7 +58,7 @@ function destroy( forms = [] ) {
*
* @return {Promise<void>|void} A promise that resolves when the token is refreshed.
*/
function refresh( field, action = 'submit' ) {
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 => {
Expand All @@ -72,7 +72,7 @@ function refresh( field, action = 'submit' ) {
*
* @param {HTMLElement} form The form element.
*/
function addHiddenField( form ) {
function addHiddenV3Field( form ) {
let field = form.querySelector( 'input[name="g-recaptcha-response"]' );
if ( ! field ) {
field = document.createElement( 'input' );
Expand All @@ -81,59 +81,26 @@ function addHiddenField( form ) {
form.appendChild( field );

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

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

/**
* 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 );
}
}

/**
* Remove the hidden reCAPTCHA v3 token field from the given form.
*
* @param {HTMLElement} form The form element.
*/
function removeHiddenField( form ) {
function removeHiddenV3Field( form ) {
const field = form.querySelector( 'input[name="g-recaptcha-response"]' );
if ( field ) {
field.parentElement.removeChild( field );
Expand All @@ -145,7 +112,7 @@ function removeHiddenField( form ) {
*
* @param {HTMLElement} el Element with the reCAPTCHA widget to refresh.
*/
function refreshWidget( el ) {
function refreshV2Widget( el ) {
const widgetId = parseInt( el.getAttribute( 'data-recaptcha-widget-id' ) );
if ( ! isNaN( widgetId ) ) {
grecaptcha.reset( widgetId );
Expand All @@ -159,76 +126,102 @@ function refreshWidget( el ) {
* @param {Function|null} onSuccess Callback to handle success. Optional.
* @param {Function|null} onError Callback to handle errors. Optional.
*/
function renderWidget( form, onSuccess = null, onError = null ) {
const submitButtons = [
...form.querySelectorAll( 'input[type="submit"], button[type="submit"]' ),
];

function renderV2Widget( form, onSuccess = null, onError = null ) {
// Common render options for reCAPTCHA v2 widget. See https://developers.google.com/recaptcha/docs/invisible#render_param for supported params.
const options = {
sitekey: siteKey,
size: isInvisible ? 'invisible' : 'normal',
isolated: true,
};

const submitButtons = [
...form.querySelectorAll( 'input[type="submit"], button[type="submit"]' )
];
submitButtons.forEach( button => {
// Refresh widget if it already exists.
if ( button.hasAttribute( 'data-recaptcha-widget-id' ) ) {
refreshWidget( button );
return;
}

// Callback when reCAPTCHA passes validation or skip flag is present.
const successCallback = () => {
onSuccess?.()
form.requestSubmit( button );
refreshWidget( button );
};

// Callback when reCAPTCHA rendering fails or expires.
const errorCallback = () => {
const retryCount = parseInt( button.getAttribute( 'data-recaptcha-retry-count' ) ) || 0;
if ( retryCount < 3 ) {
refreshWidget( button );
grecaptcha.execute( button.getAttribute( 'data-recaptcha-widget-id' ) );
button.setAttribute( 'data-recaptcha-retry-count', retryCount + 1 );
} else {
button.removeAttribute( 'data-recaptcha-retry-count' );
const message = wp.i18n.__( 'There was an error connecting with reCAPTCHA. Please reload the page and try again.', 'newspack-plugin' );
if ( onError ) {
onError( message );
// Don't render widget if the button has a data-skip-recaptcha attribute.
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?.()
form.requestSubmit( button );
refreshV2Widget( button );
};
// Callback when reCAPTCHA rendering fails or expires.
const errorCallback = () => {
const retryCount = parseInt( button.getAttribute( 'data-recaptcha-retry-count' ) ) || 0;
if ( retryCount < 3 ) {
refreshV2Widget( button );
grecaptcha.execute( button.getAttribute( 'data-recaptcha-widget-id' ) );
button.setAttribute( 'data-recaptcha-retry-count', retryCount + 1 );
} else {
// Recaptcha's default error behavior is to alert with the above message.
// eslint-disable-next-line no-alert
addErrorMessage( form, message );
const message = wp.i18n.__( 'There was an error connecting with reCAPTCHA. Please reload the page and try again.', 'newspack-plugin' );
if ( onError ) {
onError( message );
} else {
addErrorMessage( form, message );
}
}
}
}

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 {
let widgetId = button.getAttribute( 'data-recaptcha-widget-id' );
if ( ! widgetId ) {
// Render reCAPTCHA widget. See https://developers.google.com/recaptcha/docs/invisible#js_api for API reference.
widgetId = grecaptcha.render( button, {
...options,
callback: successCallback,
'error-callback': errorCallback,
'expired-callback': errorCallback,
} );
button.setAttribute( 'data-recaptcha-widget-id', widgetId );
const widgetId = grecaptcha.render( button, {
...options,
callback: successCallback,
'error-callback': errorCallback,
'expired-callback': errorCallback,
} );
button.setAttribute( 'data-recaptcha-widget-id', widgetId );
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 );
}
grecaptcha.execute( widgetId );
}
} );
} );
} );
}

/**
* 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 );
}
}

/**
Expand All @@ -250,12 +243,14 @@ function render( forms = [], onSuccess = null, onError = null ) {

formsToHandle.forEach( form => {
if ( ! form.hasAttribute( 'data-recaptcha-rendered' ) ) {
if ( isV3 ) {
addHiddenField( form );
}
if ( isV2 ) {
renderWidget( form, onSuccess, onError );
}
form.addEventListener( 'focusin', () => {
if ( isV3 ) {
addHiddenV3Field( form );
}
if ( isV2 ) {
renderV2Widget( form, onSuccess, onError );
}
} );
form.setAttribute( 'data-recaptcha-rendered', 'true' );
}
} );
Expand Down

0 comments on commit 9d3a932

Please sign in to comment.