Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
c086676
Add Friendly Captcha v2 as alternative spam prevention provider
iobrado Apr 16, 2026
3cd12f9
Add EU endpoint toggle and update settings labels
iobrado Apr 16, 2026
88cf26f
Bump Friendly Captcha version
iobrado Apr 17, 2026
c09a381
Update version
iobrado Apr 20, 2026
de4f985
Update logic to share the same Captcha interface
iobrado Apr 20, 2026
fc1fdfa
Add CaptchaDispatcher to route server-side verification to the active…
iobrado Apr 21, 2026
9d694e7
Revert label to spam prevention. Fix z-index on select
iobrado Apr 21, 2026
652b8b8
Fix docblock comment
iobrado Apr 21, 2026
dfd33eb
Reset token on form submit
iobrado Apr 21, 2026
9292a55
Invert captcha dispatcher so routes keep type-hinting CaptchaInterfac…
iobrado Apr 22, 2026
e915a1b
Address review feedback: unify captcha surface, simplify FriendlyCapt…
iobrado Apr 22, 2026
40af7ba
Rework captcha settings page to follow the Corvus flat-tab pattern
iobrado Apr 22, 2026
242c9e2
Misc fixes
iobrado Apr 24, 2026
80b94cf
Update SettingsFallback constants
iobrado Apr 24, 2026
55d9d63
Add switch-case for Captcha in EnqueueBlocks
iobrado Apr 24, 2026
50e6a0d
Rename EnqueueCaptcha to EnqueueRecaptcha
iobrado Apr 27, 2026
bb5a954
Add loadOnInit option for FriendlyCaptcha and refactor captcha JS
iobrado Apr 27, 2026
888a270
Add granular error handling for Friendly Captcha siteverify responses
iobrado Apr 27, 2026
9507ad3
Refactor FriendlyCaptcha::check() response handling
iobrado Apr 27, 2026
01c9092
Address PR review comments on FriendlyCaptcha
iobrado Apr 29, 2026
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
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@ All notable changes to this project will be documented in this file.

This projects adheres to [Semantic Versioning](https://semver.org/) and [Keep a CHANGELOG](https://keepachangelog.com/).

## [9.2.0]

### Added

- Added Friendly Captcha as an alternative spam prevention provider alongside Google reCAPTCHA. Includes site key/API key configuration, EU endpoint toggle, automatic SDK loading, and mutual exclusivity with reCAPTCHA.

## [9.1.6]

### Changed
Expand Down Expand Up @@ -1813,6 +1819,7 @@ This projects adheres to [Semantic Versioning](https://semver.org/) and [Keep a

- Initial production release.

[9.2.0]: https://github.com/infinum/eightshift-forms/compare/9.1.6...9.2.0
[9.1.6]: https://github.com/infinum/eightshift-forms/compare/9.1.5...9.1.6
[9.1.5]: https://github.com/infinum/eightshift-forms/compare/9.1.4...9.1.5
[9.1.4]: https://github.com/infinum/eightshift-forms/compare/9.1.3...9.1.4
Expand Down
8 changes: 4 additions & 4 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion eightshift-forms.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
* Description: Eightshift Forms is a complete form builder plugin that utilizes modern Block editor features with multiple third-party integrations, bringing your project to a new level.
* Author: WordPress team @Infinum
* Author URI: https://eightshift.com/
* Version: 9.1.6
* Version: 9.2.0
* Text Domain: eightshift-forms
*
* @package EightshiftForms
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@eightshift/eightshift-forms",
"version": "9.1.6",
"version": "9.2.0",
"description": "This repository contains all the tools you need to start building a modern WordPress project.",
"authors": [
{
Expand Down
32 changes: 24 additions & 8 deletions src/Blocks/components/form/assets/form.js
Original file line number Diff line number Diff line change
Expand Up @@ -573,18 +573,34 @@ export class Form {
*
* @returns {void}
*/
runFormCaptcha(formId, filter = {}) {
async runFormCaptcha(formId, filter = {}) {
if (!this.state.getStateCaptchaIsUsed()) {
return;
}

const actionName = this.state.getStateCaptchaSubmitAction();
const siteKey = this.state.getStateCaptchaSiteKey();
switch (this.state.getStateCaptchaType()) {
case StateEnum.CAPTCHA_TYPE_FRIENDLY: {
const widget = window[prefix]?.friendlyCaptcha;
const token = widget?.getResponse() ?? '';

if (this.state.getStateCaptchaIsEnterprise()) {
this.executeEnterpriseCaptcha(actionName, siteKey, formId, false, filter);
} else {
this.executeFreeCaptcha(actionName, siteKey, formId, false, filter);
this.setFormDataCaptcha({
token,
});

await this.formSubmit(formId, filter);
break;
}
default: {
const actionName = this.state.getStateCaptchaSubmitAction();
const siteKey = this.state.getStateCaptchaSiteKey();

if (this.state.getStateCaptchaIsEnterprise()) {
this.executeEnterpriseCaptcha(actionName, siteKey, formId, false, filter);
} else {
this.executeFreeCaptcha(actionName, siteKey, formId, false, filter);
}
break;
}
}
}

Expand Down Expand Up @@ -1978,7 +1994,7 @@ export class Form {
custom?.showDropdown();
}

if (!this.state.getStateSettingsDisableScrollToFieldOnFocus()) {
if (!this.state.getStateSettingsDisableScrollToFieldOnFocus()) {
this.utils.scrollAction(field);
}

Expand Down
111 changes: 111 additions & 0 deletions src/Blocks/components/form/assets/friendly-captcha.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
/* global frcaptcha */

import { prefix, setStateWindow } from './state-init';

/**
* FriendlyCaptcha class.
*/
export class FriendlyCaptcha {
constructor(utils) {
/** @type {import('./utils').Utils} */
this.utils = utils;
/** @type {import('./state').State} */
this.state = this.utils.getState();

this.widget = null;

// Set all public methods.
this.publicMethods();
}

////////////////////////////////////////////////////////////////
// Public methods
////////////////////////////////////////////////////////////////

/**
* Init all actions.
*
* @returns {void}
*/
init() {
if (!this.state.getStateCaptchaIsUsed()) {
return;
}

if (!this.state.getStateCaptchaLoadOnInit() && !document.querySelectorAll(this.state.getStateSelector('form', true))?.length) {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the whole point of loading the cpactha on every page i not to limit the load on the sites that have forms!

return;
}

this.initWidget();
this.initResetOnSubmit();
}

/**
* Reset widget after each form submission so a fresh token is ready for the next attempt.
*
* @returns {void}
*/
initResetOnSubmit() {
window.addEventListener(this.state.getStateEvent('afterFormSubmit'), () => {
this.widget?.reset();
});
}

/**
* Initialize Friendly Captcha widget.
*
* @returns {void}
*/
initWidget() {
if (typeof frcaptcha === 'undefined') {
return;
}

const siteKey = this.state.getStateCaptchaSiteKey();

// Create a hidden container for the widget.
const container = document.createElement('div');
container.style.display = 'none';
document.body.appendChild(container);

this.widget = frcaptcha.createWidget({
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You should initialize this.widget to a null or empty object in the constructor, since it's referenced elsewhere in the class.

element: container,
sitekey: siteKey,
startMode: 'auto',
apiEndpoint: this.state.getStateCaptchaEndpoint(),
});
}

////////////////////////////////////////////////////////////////
// Private methods - not shared to the public window object.
////////////////////////////////////////////////////////////////

/**
* Set all public methods.
*
* @returns {void}
*/
publicMethods() {
setStateWindow();

if (window[prefix].friendlyCaptcha) {
return;
}

window[prefix].friendlyCaptcha = {
init: () => {
this.init();
},
initWidget: () => {
this.initWidget();
},
initResetOnSubmit: () => {
this.initResetOnSubmit();
},
getResponse: () => this.widget?.getResponse() ?? '',
reset: () => {
this.widget?.reset();
},
};
}
}
18 changes: 13 additions & 5 deletions src/Blocks/components/form/assets/index.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/* global esFormsLocalization */

import domReady from '@wordpress/dom-ready';
import { setStateInitial } from './state-init';
import { StateEnum, setStateInitial } from './state-init';
import { Utils } from './utils';

// Global variable must be set for everything to work.
Expand All @@ -17,11 +17,19 @@ const utils = new Utils();
const state = utils.getState();

domReady(() => {
// Load captcha if using initial.
if (state.getStateCaptchaIsUsed()) {
import('./captcha').then(({ Captcha }) => {
new Captcha(utils).init();
});
switch (state.getStateCaptchaType()) {
case StateEnum.CAPTCHA_TYPE_FRIENDLY:
import('./friendly-captcha').then(({ FriendlyCaptcha }) => {
new FriendlyCaptcha(utils).init();
});
break;
default:
import('./captcha').then(({ Captcha }) => {
new Captcha(utils).init();
});
break;
}
}

if (!state.getStateSettingsFormDisableAutoInit()) {
Expand Down
24 changes: 19 additions & 5 deletions src/Blocks/components/form/assets/state-init.js
Original file line number Diff line number Diff line change
Expand Up @@ -98,12 +98,17 @@ export const StateEnum = {
SETTINGS_FORM_MISCONFIGURED_MSG: 'formMisconfigured',

CAPTCHA: 'captcha',
CAPTCHA_TYPE: 'type',
CAPTCHA_SITE_KEY: 'site_key',
CAPTCHA_IS_ENTERPRISE: 'isEnterprise',
CAPTCHA_SUBMIT_ACTION: 'submitAction',
CAPTCHA_INIT_ACTION: 'initAction',
CAPTCHA_LOAD_ON_INIT: 'loadOnInit',
CAPTCHA_HIDE_BADGE: 'hideBadge',
CAPTCHA_ENDPOINT: 'endpoint',

CAPTCHA_TYPE_GOOGLE: 'google',
CAPTCHA_TYPE_FRIENDLY: 'friendly',

ENRICHMENT: 'enrichment',
ENRICHMENT_FORM_PREFILL: 'formPrefill',
Expand Down Expand Up @@ -255,17 +260,26 @@ export function setStateInitial() {
setState([StateEnum.SETTINGS_FORM_CAPTCHA_ERROR_MSG], esFormsLocalization.formCaptchaErrorMsg ?? '', StateEnum.SETTINGS);
setState([StateEnum.SETTINGS_FORM_MISCONFIGURED_MSG], esFormsLocalization.formMisconfigured ?? '', StateEnum.SETTINGS);

// Captcha.
// Captcha — single payload discriminated by `type`.
const captcha = esFormsLocalization.captcha ?? {};
setState([StateEnum.IS_USED], Boolean(captcha.isUsed), StateEnum.CAPTCHA);

if (captcha.isUsed) {
setState([StateEnum.CAPTCHA_TYPE], captcha.type, StateEnum.CAPTCHA);
setState([StateEnum.CAPTCHA_SITE_KEY], captcha.siteKey, StateEnum.CAPTCHA);
setState([StateEnum.CAPTCHA_IS_ENTERPRISE], Boolean(captcha.isEnterprise), StateEnum.CAPTCHA);
setState([StateEnum.CAPTCHA_SUBMIT_ACTION], captcha.submitAction, StateEnum.CAPTCHA);
setState([StateEnum.CAPTCHA_INIT_ACTION], captcha.initAction, StateEnum.CAPTCHA);
setState([StateEnum.CAPTCHA_LOAD_ON_INIT], Boolean(captcha.loadOnInit), StateEnum.CAPTCHA);
setState([StateEnum.CAPTCHA_HIDE_BADGE], Boolean(captcha.hideBadge), StateEnum.CAPTCHA);

switch (captcha.type) {
case StateEnum.CAPTCHA_TYPE_FRIENDLY:
setState([StateEnum.CAPTCHA_ENDPOINT], captcha.endpoint, StateEnum.CAPTCHA);
break;
default:
setState([StateEnum.CAPTCHA_IS_ENTERPRISE], Boolean(captcha.isEnterprise), StateEnum.CAPTCHA);
setState([StateEnum.CAPTCHA_SUBMIT_ACTION], captcha.submitAction, StateEnum.CAPTCHA);
setState([StateEnum.CAPTCHA_INIT_ACTION], captcha.initAction, StateEnum.CAPTCHA);
setState([StateEnum.CAPTCHA_HIDE_BADGE], Boolean(captcha.hideBadge), StateEnum.CAPTCHA);
break;
}
}

// Geolocation.
Expand Down
6 changes: 6 additions & 0 deletions src/Blocks/components/form/assets/state.js
Original file line number Diff line number Diff line change
Expand Up @@ -430,6 +430,12 @@ export class State {
getStateCaptchaHideBadge = () => {
return getState([StateEnum.CAPTCHA_HIDE_BADGE], StateEnum.CAPTCHA);
};
getStateCaptchaType = () => {
return getState([StateEnum.CAPTCHA_TYPE], StateEnum.CAPTCHA);
};
getStateCaptchaEndpoint = () => {
return getState([StateEnum.CAPTCHA_ENDPOINT], StateEnum.CAPTCHA);
};

////////////////////////////////////////////////////////////////
// Geolocation getters.
Expand Down
Loading