verbb / formie

The most user-friendly forms plugin for Craft CMS.
Other
95 stars 72 forks source link

Failed Captcha “Recaptcha”: “Client-side token missing.” on a form submitted via Javascript. #2017

Closed olihar closed 1 month ago

olihar commented 1 month ago

Describe the bug

Not sure if this is a bug or something I've got wrong. Note the ReCaptcha works as expected on forms that are submitted in the normal way. This is only for one form, that I submit via Javascript.

So the form is intercepted by javascript - if a specific ID is present it needs to run a function. On success of this function I do the following

form.form.submitForm();

This submits the form and saves the data - however it ends up in spam with the error

Failed Captcha “Recaptcha”: “Client-side token missing.”

When I submit the form using the Javascript submitForm function do I need to do something to include the client side token that is missing?

On a side note - that may shed some light on things, when I add the following to my js file

document.addEventListener('onFormieInit', (e) => { console.log('formie initialised'); // Fetch the Form Factory once it's been loaded let Formie = e.detail.formie; let form = Formie.getFormByHandle('myForm'); });

I don't get a console.log message. It looks like the formie.js is loaded in the page source and I've put the above inside a DOMContentLoaded event listener but still no joy.

Not sure if this affects anything though, because if the form is submitted normally (i.e. not via javascript) I don't have a problem.

Steps to reproduce

  1. Submit form via Javascript using submitForm() function
  2. Form submits but details are marked as spam with the error Failed Captcha “Recaptcha”: “Client-side token missing.”

Form settings

Craft CMS version

4.10.7

Plugin version

dev-craft-4

Multi-site?

No

Additional context

No response

engram-design commented 1 month ago

So this is expected behaviour when calling submitForm() via JS, purely because that function doesn't handle a few pre-submit tasks.

Take a look at our submit event listener:

https://github.com/verbb/formie/blob/7c35d8b7a1cb644a156efabeb99e261a120a0223/src/web/assets/frontend/src/js/formie-form-base.js#L26-L56

You'll notice the process goes like:

So if you're overriding Formie's event listener on the submit event, or doing something in the onBeforeFormieSubmit event, then calling submitForm() you're skipping a bunch of things Formie normally does.

To better cater for this, so you don't have to call everything yourself, I've added a processSubmit() function in the dev-craft-4 branch. This is the function you should call when you want to submit the form as normal, with all the bits you'd expect.

https://github.com/verbb/formie/blob/8b4614df69765855520136f645d41fd1e305818d/src/web/assets/frontend/src/js/formie-form-base.js#L41-L62

olihar commented 1 month ago

Thank you, this sounds amazing. I've updated to composer require verbb/formie:"dev-craft-4 as 2.1.24" and changed

form.form.submitForm();

to

form.form.processSubmit();

the form submits, however the submission is still going into spam with the same error message.

engram-design commented 1 month ago

Hmm, Are you able to send through a minimal example of your JS for submitting the form, and anything else (forms-related) you're doing just so I can check?

olihar commented 1 month ago

thank you for coming back so quickly, please see the javascript below.

`document.addEventListener('DOMContentLoaded', function() { const form = document.querySelector('#formOne form'); if(form){ const billingReqID = form.querySelector('input[name="fields[billingRequestFlowID]"]'); if(billingReqID && billingReqID.value != ''){ const billingReqIDValue = billingReqID.value; console.log('hidden field present', billingReqIDValue); initiateGoCardlessPayment(billingReqIDValue, form); }else{ console.log('no hidden field'); }

}

}); function initiateGoCardlessPayment(billingReqIDValue, form) { if (billingReqIDValue) { console.log('Initiating GoCardless with billingRequestFlowID:', billingReqIDValue); const handler = GoCardlessDropin.create({ billingRequestFlowID: billingReqIDValue, environment: 'sandbox', onSuccess: (billingRequest, billingRequestFlow) => { console.log('Payment successful'); const mandateID = billingRequest.mandate_request.links.mandate; const mandateField = form.querySelector('input[name="fields[referenceNumber]"]'); mandateField.value = mandateID; const customerID = billingRequest.resources.customer.id; const customerField = form.querySelector('input[name="fields[customerId]"]'); customerField.value = customerID; handler.exit(); form.form.processSubmit(); }, onExit: (error, metadata) => { console.log('Payment failed', error); form.form.formSubmitError(); const redirectUrl = 'https://............./form-testing/error/'; window.location.href = redirectUrl; } }); handler.open(); } else { console.error('Billing request flow ID not set.'); form.form.formSubmitError(); } }`

(apologies if my code is terrible, we're just in the early stages or testing/playing around with what's possible)

engram-design commented 1 month ago

Right, so you're essentially doing something like:

setTimeout(function() {
    const $form = document.querySelector('form');

    $form.form.processSubmit();
}, 2000)

Which is just an example that submits the form on page load (with a delay), not using any events, just straight submitting the form. That's a slightly different way to what I imagined, and is more a programmatic way of submitting the form, and not relying on the submit event of a form. That's fair enough.

For this use-case, I've further split things, as you're missing out on the onBeforeFormieSubmit event that some things will need to use. The reason I mentioned processSubmit() is that this is often called when you're listening to this event, and you don't want the onBeforeFormieSubmit called again (because you're already inside it), but if your case that's not what you're doing.

So instead (sorry!) you can use the initSubmit() event. Just run a composer require verbb/formie:"dev-craft-4 as 2.1.24"

That's working for me, and I can see recaptcha loading it's client-side token with that.

olihar commented 1 month ago

Thank you for your help, unfortunately I'm still getting the same error and the entry is going into spam. I'm using

form.form.initSubmit();

When I use dev tools under network - form page - payload the 'g-recaptcha-response' is empty. Sorry, I'm not sure where I'm going wrong?

engram-design commented 1 month ago

Ah, no you're right. I believe you'll need to also include the submitAction for the form, which is used to determine whether you're going back, saving, or submitting the form. This is usually automatically done by Formie and the submit action, in combination with a button-click event handler to figure out what sort of button you're pressing.

form.form.submitAction = 'submit';

Add that before submitting and see how that goes.

engram-design commented 1 month ago

Updated in 2.1.25

olihar commented 1 month ago

Amazing, that worked, thank you so much for all of your help :)