listmonk icon indicating copy to clipboard operation
listmonk copied to clipboard

Support for invisible hCaptcha

Open candidexmedia opened this issue 11 months ago • 3 comments

Is your feature request related to a problem? Please describe.

I recently learned about invisible captchas, and found out that hCaptcha supports them. Implementing it on the embedded form on my website was fairly straightforward and works beautifully! The initial implementation of hCaptcha was clunky, and didn't mesh well with the website design. This option is a lot more seamless.

Image

Describe the solution you'd like

I would love to see an option for invisible captchas in the provided Listmonk embed code (this could be an additional checkbox in the Settings > Security captcha section, or a checkbox in the Form page).

To implement it, I had to do the following (updated instructions):

  1. Add an id attribute to the Listmonk form

  2. Add an id attribute to the submit button

  3. Add data-callback="onSubmit" and data-size="invisible" attributes to the hcaptcha <div>

  4. Add an additional script before the hosted one

The final form code looks like this:

<form method="post" action="https://mylistmonkinstance.com/subscription/form" target="_blank" class="listmonk-form" id="listmonk-form">
  <div>
        <h3>Subscribe</h3>
        <input type="hidden" name="nonce" />
    
        <p><input type="email" name="email" required placeholder="E-mail" /></p>
        <p><input type="text" name="name" placeholder="Name (optional)" /></p>
    
        <p>
          <input id="321" type="checkbox" name="l" checked value="54321" />
          <label for="321">My List</label>
        </p>

    <div class="h-captcha" data-sitekey="1234567" data-callback="onSubmit" data-size="invisible"></div>

    <input type="submit" value="Submit" id="listmonk-form-submit">
            
    <script type="text/javascript">
        const form = document.getElementById('listmonk-form');

	function validate(event) { // add back client-side validation
		event.preventDefault();
		if (form.checkValidity()) {
			hcaptcha.execute(); // if the form is valid, run captcha
		} else {
			form.reportValidity(); // if the form is invalid, display the error messages
		}
	}

	function onLoad() {
		var element = document.getElementById('listmonk-form-submit');
		element.onclick = validate; // run the validate function when the submit button is clicked
	}

	function onSubmit(token) {
		form.submit(); // submit the form if the hcaptcha is succesful
	}
    </script>

    <script src="https://js.hcaptcha.com/1/api.js?onload=onLoad" async defer></script>

  </div>
</form>

old instructions for binding the challenge to a button (but interferes with client-side validation)
  1. Add an id to the Listmonk form

  2. Remove the hcaptcha divs:

    <div class="h-captcha" data-sitekey="1234567"></div>
    <script src="https://js.hcaptcha.com/1/api.js" async defer></script>
    
  3. Add an h-captcha class, and data-sitekey="your_site_key" + data-callback="onSubmit" attributes to the submit button input, like this:

    <input type="submit" value="Submit" class="h-captcha" data-sitekey="1234567" data-callback="onSubmit"/>
    
  4. Add a data callback and the hCaptcha script:

        <script type="text/javascript">
            function onSubmit(token) {
               document.getElementById('listmonk-form').submit();
            }
         </script>
         <script src="https://js.hcaptcha.com/1/api.js" async defer></script>
    

The final form code looks like this:

    <form method="post" action="https://mylistmonkinstance.com/subscription/form" target="_blank" class="listmonk-form" id="listmonk-form">
        <div>
            <h3>Subscribe</h3>
            <input type="hidden" name="nonce" />
        
            <p><input type="email" name="email" required placeholder="E-mail" /></p>
            <p><input type="text" name="name" placeholder="Name (optional)" /></p>
        
            <p>
              <input id="321" type="checkbox" name="l" checked value="54321" />
              <label for="321">My List</label>
            </p>
        
            <input type="submit" value="Submit" class="h-captcha" data-sitekey="1234567" data-callback="onSubmit"/>
                    
            <script type="text/javascript">
                        function onSubmit(token) {
                           document.getElementById('listmonk-form').submit();
                        }
            </script>
            <script src="https://js.hcaptcha.com/1/api.js" async defer></script>
        </div>
    </form> 

candidexmedia avatar Feb 07 '25 07:02 candidexmedia

Thank you @candidexmedia this is cool! I've implemented it but I see form validation doesn't seem to work anymore following this way. As the captcha gets validated, it directly submit the form and bypasses any browser validation.

So if there's an error on the email or name field, user only understands after submitting and not while still being on the form.

I wonder if you found a way to keep hCaptacha invisible while not losing browser form validation?

ImaCrea avatar May 12 '25 15:05 ImaCrea

@ImaCrea Thank you for flagging this! I've read through the hcaptcha docs, did some digging, and rewrote the whole thing so that the client-side validation goes through first:

The final form code looks something like this:

<form method="post" action="https://mylistmonkinstance.com/subscription/form" target="_blank" class="listmonk-form" id="listmonk-form">
  <div>
        <h3>Subscribe</h3>
        <input type="hidden" name="nonce" />
    
        <p><input type="email" name="email" required placeholder="E-mail" /></p>
        <p><input type="text" name="name" placeholder="Name (optional)" /></p>
    
        <p>
          <input id="321" type="checkbox" name="l" checked value="54321" />
          <label for="321">My List</label>
        </p>

    <div class="h-captcha" data-sitekey="1234567" data-callback="onSubmit" data-size="invisible"></div>

    <input type="submit" value="Submit" id="listmonk-form-submit">
            
    <script type="text/javascript">
        const form = document.getElementById('listmonk-form');

	function validate(event) { // add back client-side validation
		event.preventDefault();
		if (form.checkValidity()) {
			hcaptcha.execute(); // if the form is valid, run captcha
		} else {
			form.reportValidity(); // if the form is invalid, display the error messages
		}
	}

	function onLoad() {
		var element = document.getElementById('listmonk-form-submit');
		element.onclick = validate; // run the validate function when the submit button is clicked
	}

	function onSubmit(token) {
		form.submit(); // submit the form if the hcaptcha is succesful
	}
    </script>

    <script src="https://js.hcaptcha.com/1/api.js?onload=onLoad" async defer></script>

  </div>
</form>

Main changes from the previous version:

  • added back the hcaptcha div
  • added data-callback="onSubmit" and data-size="invisible" attributes to the hcaptcha <div>
  • removed hcaptcha HTML attributes that would bind the challenge to the submit button
  • modified the first script so that it runs the client-side validation first
  • gave the submit button an ID
  • added ?onload=onLoad to the hcaptcha script src URL because I saw it in the docs example. Is it actually needed? No clue, lol...

You can see it in action on my homepage: www.candide.media

demo clip

candidexmedia avatar May 13 '25 09:05 candidexmedia

Working nicely @candidexmedia ! Thanks for sharing, it's very cool

ImaCrea avatar May 13 '25 10:05 ImaCrea

This issue has been marked 'stale' after 90 days of inactivity. If there is no further activity, it will be closed in 7 days.

github-actions[bot] avatar Aug 12 '25 02:08 github-actions[bot]